From f648310acbdd7da252fe03ad4bf8c30b433be64a Mon Sep 17 00:00:00 2001 From: Chris Martinez Date: Sun, 18 Sep 2016 13:01:11 -0700 Subject: [PATCH] Initial set of acceptance tests (#32) --- ApiVersioning.sln | 14 ++ .../AcceptanceTest.cs | 138 +++++++++++++++ .../Http/Basic/BasicAcceptanceTest.cs | 25 +++ .../Basic/Controllers/HelloWorldController.cs | 19 +++ .../Basic/Controllers/Values2Controller.cs | 12 ++ .../Basic/Controllers/ValuesController.cs | 12 ++ ...oned_ApiController_split_into_two_types.cs | 53 ++++++ .../given/a_url_versioned_ApiController.cs | 71 ++++++++ .../ByNamespace/ByNamespaceAcceptanceTest.cs | 31 ++++ .../Controllers/V1/AgreementsController.cs | 12 ++ .../Controllers/V2/AgreementsController.cs | 12 ++ .../Controllers/V3/AgreementsController.cs | 12 ++ .../Http/ByNamespace/Models/Agreement.cs | 20 +++ ...g_versioned_ApiController_per_namespace.cs | 54 ++++++ ...l_versioned_ApiController_per_namespace.cs | 54 ++++++ .../Controllers/HelloWorldController.cs | 14 ++ .../Controllers/Values2Controller.cs | 20 +++ .../Controllers/ValuesController.cs | 14 ++ .../Conventions/ConventionsAcceptanceTest.cs | 40 +++++ ..._split_into_two_types_using_conventions.cs | 54 ++++++ ...rsioned_ApiController_using_conventions.cs | 59 +++++++ .../HttpContentExtensions.cs | 11 ++ .../HttpSimulatorHandler.cs | 74 ++++++++ ...osoft.AspNet.WebApi.Acceptance.Tests.xproj | 22 +++ .../OData/Advanced/AdvancedAcceptanceTest.cs | 54 ++++++ .../Advanced/Controllers/Orders2Controller.cs | 23 +++ .../Advanced/Controllers/Orders3Controller.cs | 18 ++ .../Advanced/Controllers/OrdersController.cs | 12 ++ .../Advanced/Controllers/People2Controller.cs | 23 +++ .../Advanced/Controllers/PeopleController.cs | 39 +++++ ...le_ODataController_split_into_two_types.cs | 158 ++++++++++++++++++ ..._ApiController_using_convention_routing.cs | 68 ++++++++ .../given/a_v2_orders_ODataController.cs | 40 +++++ ...s_ApiController_using_attribute_routing.cs | 40 +++++ .../OData/Basic/BasicAcceptanceTest.cs | 69 ++++++++ .../Basic/Controllers/OrdersController.cs | 22 +++ .../Basic/Controllers/People2Controller.cs | 23 +++ .../Basic/Controllers/PeopleController.cs | 39 +++++ ..._query_string_versioned_ODataController.cs | 44 +++++ ...ed_ODataController_split_into_two_types.cs | 79 +++++++++ .../given/a_url_versioned_ODataController.cs | 44 +++++ ...ed_ODataController_split_into_two_types.cs | 64 +++++++ .../Configuration/OrderModelConfiguration.cs | 39 +++++ .../Configuration/PersonModelConfiguration.cs | 44 +++++ .../Controllers/OrdersController.cs | 20 +++ .../Controllers/People2Controller.cs | 22 +++ .../Controllers/PeopleController.cs | 37 ++++ .../Conventions/ConventionsAcceptanceTest.cs | 49 ++++++ ..._split_into_two_types_using_conventions.cs | 79 +++++++++ ...ioned_ODataController_using_conventions.cs | 44 +++++ ..._split_into_two_types_using_conventions.cs | 64 +++++++ ...ioned_ODataController_using_conventions.cs | 44 +++++ .../OData/Models/Order.cs | 20 +++ .../OData/Models/Person.cs | 23 +++ .../OData/ODataAcceptanceTest.cs | 108 ++++++++++++ .../OneApiError.cs | 13 ++ .../OneApiErrorResponse.cs | 9 + .../OneApiInnerError.cs | 11 ++ .../Properties/AssemblyInfo.cs | 19 +++ .../TaskExtensions.cs | 16 ++ .../app.config | 6 + .../project.json | 32 ++++ .../AcceptanceTest.cs | 137 +++++++++++++++ .../Basic/BasicAcceptanceTest.cs | 18 ++ .../Basic/Controllers/HelloWorldController.cs | 19 +++ .../Basic/Controllers/Values2Controller.cs | 13 ++ .../Basic/Controllers/ValuesController.cs | 13 ++ ...rsioned_Controller_split_into_two_types.cs | 53 ++++++ .../Basic/given/a_url_versioned_Controller.cs | 71 ++++++++ .../ByNamespace/ByNamespaceAcceptanceTest.cs | 25 +++ .../Controllers/V1/AgreementsController.cs | 10 ++ .../Controllers/V2/AgreementsController.cs | 10 ++ .../Controllers/V3/AgreementsController.cs | 10 ++ .../ByNamespace/Models/Agreement.cs | 20 +++ ...ring_versioned_Controller_per_namespace.cs | 47 ++++++ ..._url_versioned_Controller_per_namespace.cs | 47 ++++++ .../Controllers/HelloWorldController.cs | 16 ++ .../Controllers/Values2Controller.cs | 21 +++ .../Controllers/ValuesController.cs | 15 ++ .../Conventions/ConventionsAcceptanceTest.cs | 32 ++++ ..._split_into_two_types_using_conventions.cs | 48 ++++++ ..._versioned_Controller_using_conventions.cs | 59 +++++++ .../FilteredControllerFeatureProvider.cs | 32 ++++ .../HttpContentExtensions.cs | 11 ++ ...soft.AspNetCore.Mvc.Acceptance.Tests.xproj | 22 +++ .../OneApiError.cs | 13 ++ .../OneApiErrorResponse.cs | 9 + .../OneApiInnerError.cs | 11 ++ .../Properties/AssemblyInfo.cs | 19 +++ .../TaskExtensions.cs | 16 ++ .../project.json | 37 ++++ .../xunit.runner.json | 3 + 92 files changed, 3262 insertions(+) create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/AcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/BasicAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/HelloWorldController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/Values2Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/ValuesController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_query_string_versioned_ApiController_split_into_two_types.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_url_versioned_ApiController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/ByNamespaceAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V1/AgreementsController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V2/AgreementsController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V3/AgreementsController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Models/Agreement.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_query_string_versioned_ApiController_per_namespace.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_url_versioned_ApiController_per_namespace.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/HelloWorldController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/Values2Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/ValuesController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/ConventionsAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_query_string_versioned_ApiController_split_into_two_types_using_conventions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_url_versioned_ApiController_using_conventions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpContentExtensions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpSimulatorHandler.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Microsoft.AspNet.WebApi.Acceptance.Tests.xproj create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_mix-in_people_ODataController_split_into_two_types.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v1_orders_ApiController_using_convention_routing.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v2_orders_ODataController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v3_orders_ApiController_using_attribute_routing.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController_split_into_two_types.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController_split_into_two_types.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_split_into_two_types_using_conventions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_using_conventions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_split_into_two_types_using_conventions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_using_conventions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Order.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Person.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiError.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiErrorResponse.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiInnerError.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/TaskExtensions.cs create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/app.config create mode 100644 test/Microsoft.AspNet.WebApi.Acceptance.Tests/project.json create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/AcceptanceTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/BasicAcceptanceTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/HelloWorldController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/Values2Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/ValuesController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_query_string_versioned_Controller_split_into_two_types.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_url_versioned_Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/ByNamespaceAcceptanceTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V1/AgreementsController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V2/AgreementsController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V3/AgreementsController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Models/Agreement.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_query_string_versioned_Controller_per_namespace.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_url_versioned_Controller_per_namespace.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/HelloWorldController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/Values2Controller.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/ValuesController.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/ConventionsAcceptanceTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_query_string_versioned_Controller_split_into_two_types_using_conventions.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_url_versioned_Controller_using_conventions.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/FilteredControllerFeatureProvider.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpContentExtensions.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Microsoft.AspNetCore.Mvc.Acceptance.Tests.xproj create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiError.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiErrorResponse.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiInnerError.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/TaskExtensions.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/project.json create mode 100644 test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/xunit.runner.json diff --git a/ApiVersioning.sln b/ApiVersioning.sln index 553de62b..8d5eb4ed 100644 --- a/ApiVersioning.sln +++ b/ApiVersioning.sln @@ -97,6 +97,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Conventions", "Conventions" src\Common\Versioning\Conventions\IApiVersionConventionT.cs = src\Common\Versioning\Conventions\IApiVersionConventionT.cs EndProjectSection EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.WebApi.Acceptance.Tests", "test\Microsoft.AspNet.WebApi.Acceptance.Tests\Microsoft.AspNet.WebApi.Acceptance.Tests.xproj", "{5C31964D-EA8B-420B-9297-5ADFEFE54962}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Mvc.Acceptance.Tests", "test\Microsoft.AspNetCore.Mvc.Acceptance.Tests\Microsoft.AspNetCore.Mvc.Acceptance.Tests.xproj", "{4EED304C-D1A6-4866-8D7F-450D084FD25D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -127,6 +131,14 @@ Global {D87E54CC-C2D6-4AE5-806D-AE825B051C66}.Debug|Any CPU.Build.0 = Debug|Any CPU {D87E54CC-C2D6-4AE5-806D-AE825B051C66}.Release|Any CPU.ActiveCfg = Release|Any CPU {D87E54CC-C2D6-4AE5-806D-AE825B051C66}.Release|Any CPU.Build.0 = Release|Any CPU + {5C31964D-EA8B-420B-9297-5ADFEFE54962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C31964D-EA8B-420B-9297-5ADFEFE54962}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C31964D-EA8B-420B-9297-5ADFEFE54962}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C31964D-EA8B-420B-9297-5ADFEFE54962}.Release|Any CPU.Build.0 = Release|Any CPU + {4EED304C-D1A6-4866-8D7F-450D084FD25D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4EED304C-D1A6-4866-8D7F-450D084FD25D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4EED304C-D1A6-4866-8D7F-450D084FD25D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4EED304C-D1A6-4866-8D7F-450D084FD25D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,5 +156,7 @@ Global {AEB074E1-E57A-4DD3-A972-3625B367CE5D} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} {D87E54CC-C2D6-4AE5-806D-AE825B051C66} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} {B24995FB-AF48-4E5D-9327-377A599BDE2A} = {DE4EE45F-F8EA-4B32-B16F-441F946ACEF4} + {5C31964D-EA8B-420B-9297-5ADFEFE54962} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} + {4EED304C-D1A6-4866-8D7F-450D084FD25D} = {0987757E-4D09-4523-B9C9-65B1E8832AA1} EndGlobalSection EndGlobal diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/AcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/AcceptanceTest.cs new file mode 100644 index 00000000..f6211e10 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/AcceptanceTest.cs @@ -0,0 +1,138 @@ +namespace Microsoft.Web +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Net.Http.Formatting; + using System.Net.Http.Headers; + using System.Threading.Tasks; + using System.Web.Http; + using System.Web.Http.Dispatcher; + using Xunit; + using static System.Net.Http.HttpMethod; + using static System.String; + using static System.Web.Http.IncludeErrorDetailPolicy; + + [Trait( "Kind", "Acceptance" )] + public abstract class AcceptanceTest : IDisposable + { + private sealed class FilteredControllerTypeResolver : List, IHttpControllerTypeResolver + { + public ICollection GetControllerTypes( IAssembliesResolver assembliesResolver ) => this; + } + + private const string JsonMediaType = "application/json"; + private static readonly HttpMethod Patch = new HttpMethod( "PATCH" ); + private readonly FilteredControllerTypeResolver filteredControllerTypes = new FilteredControllerTypeResolver(); + private bool disposed; + + ~AcceptanceTest() + { + Dispose( false ); + } + + protected AcceptanceTest() + { + Configuration.IncludeErrorDetailPolicy = Always; + Configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), FilteredControllerTypes ); + Server = new HttpServer( Configuration ); + Client = new HttpClient( new HttpSimulatorHandler( Server ) ) + { + BaseAddress = new Uri( "http://localhost" ), + DefaultRequestHeaders = + { + { "Host", "localhost" } + } + }; + } + + protected HttpConfiguration Configuration { get; } = new HttpConfiguration(); + + protected HttpServer Server { get; } + + protected HttpClient Client { get; } + + protected IList FilteredControllerTypes => filteredControllerTypes; + + protected virtual void Dispose( bool disposing ) + { + if ( disposed ) + { + return; + } + + disposed = true; + + if ( !disposing ) + { + return; + } + + Client.Dispose(); + Server.Dispose(); + Configuration.Dispose(); + } + + public void Dispose() + { + Dispose( true ); + GC.SuppressFinalize( this ); + } + + private HttpRequestMessage CreateRequest( string requestUri, TEntity entity, HttpMethod method ) + { + var request = new HttpRequestMessage( method, requestUri ); + + if ( !Equals( entity, default( TEntity ) ) ) + { + var formatter = new JsonMediaTypeFormatter(); + request.Content = new ObjectContent( entity, formatter, JsonMediaType ); + } + + Client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue( JsonMediaType ) ); + + return request; + } + + protected void Accept( string metadata = null ) + { + var mediaType = new MediaTypeWithQualityHeaderValue( JsonMediaType ); + var odataMetadata = new NameValueHeaderValue( "odata.metadata" ); + + if ( IsNullOrEmpty( metadata ) ) + { + odataMetadata.Value = "none"; + } + else + { + switch ( metadata.ToUpperInvariant() ) + { + case "NONE": + case "MINIMAL": + case "FULL": + break; + default: + throw new ArgumentOutOfRangeException( nameof( metadata ), "The specified metadata value must be 'none', 'minimal', or 'full'." ); + } + + odataMetadata.Value = metadata; + } + + mediaType.Parameters.Add( odataMetadata ); + Client.DefaultRequestHeaders.Accept.Clear(); + Client.DefaultRequestHeaders.Accept.Add( mediaType ); + } + + protected void PreferNoReturn() => Client.DefaultRequestHeaders.Add( "Prefer", "return=representation" ); + + protected virtual Task GetAsync( string requestUri ) => Client.SendAsync( CreateRequest( requestUri, default( object ), Get ) ); + + protected virtual Task PostAsync( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Post ) ); + + protected virtual Task PutAsync( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Put ) ); + + protected virtual Task PatchAsync( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Patch ) ); + + protected virtual Task DeleteAsync( string requestUri ) => Client.SendAsync( CreateRequest( requestUri, default( object ), Delete ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/BasicAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/BasicAcceptanceTest.cs new file mode 100644 index 00000000..83a29501 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/BasicAcceptanceTest.cs @@ -0,0 +1,25 @@ +namespace Microsoft.Web.Http.Basic +{ + using Controllers; + using Microsoft.Web.Http.Routing; + using System.Web.Http; + using System.Web.Http.Routing; + + public abstract class BasicAcceptanceTest : AcceptanceTest + { + protected BasicAcceptanceTest() + { + var constraintResolver = new DefaultInlineConstraintResolver() + { + ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) } + }; + + FilteredControllerTypes.Add( typeof( ValuesController ) ); + FilteredControllerTypes.Add( typeof( Values2Controller ) ); + FilteredControllerTypes.Add( typeof( HelloWorldController ) ); + Configuration.AddApiVersioning( options => options.ReportApiVersions = true ); + Configuration.MapHttpAttributeRoutes( constraintResolver ); + Configuration.EnsureInitialized(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/HelloWorldController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/HelloWorldController.cs new file mode 100644 index 00000000..71882cf6 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/HelloWorldController.cs @@ -0,0 +1,19 @@ +namespace Microsoft.Web.Http.Basic.Controllers +{ + using Microsoft.Web.Http; + using System.Web.Http; + + [ApiVersion( "1.0" )] + [RoutePrefix( "api/v{version:apiVersion}/helloworld" )] + public class HelloWorldController : ApiController + { + [Route] + public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route( "{id:int}", Name = "GetMessageById" )] + public IHttpActionResult Get( int id ) => Ok( new { controller = GetType().Name, id = id, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route] + public IHttpActionResult Post() => CreatedAtRoute( "GetMessageById", new { id = 42 }, default( object ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/Values2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/Values2Controller.cs new file mode 100644 index 00000000..025e6608 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/Values2Controller.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Web.Http.Basic.Controllers +{ + using Microsoft.Web.Http; + using System.Web.Http; + + [ApiVersion( "2.0" )] + [Route( "api/values" )] + public class Values2Controller : ApiController + { + public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/ValuesController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/ValuesController.cs new file mode 100644 index 00000000..bc7043cb --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/Controllers/ValuesController.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Web.Http.Basic.Controllers +{ + using Microsoft.Web.Http; + using System.Web.Http; + + [ApiVersion( "1.0" )] + [Route( "api/values" )] + public class ValuesController : ApiController + { + public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_query_string_versioned_ApiController_split_into_two_types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_query_string_versioned_ApiController_split_into_two_types.cs new file mode 100644 index 00000000..5deeffa0 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_query_string_versioned_ApiController_split_into_two_types.cs @@ -0,0 +1,53 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.Http.Basic; + using Microsoft.Web.Http.Basic.Controllers; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ApiController_split_into_two_types : BasicAcceptanceTest + { + [Theory] + [InlineData( nameof( ValuesController ), "1.0" )] + [InlineData( nameof( Values2Controller ), "2.0" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + + + // act + var response = await GetAsync( $"api/values?api-version={apiVersion}" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" ); + content.ShouldBeEquivalentTo( + new Dictionary() + { + ["controller"] = controller, + ["version"] = apiVersion + } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/values?api-version=3.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_url_versioned_ApiController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_url_versioned_ApiController.cs new file mode 100644 index 00000000..629fab2a --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Basic/given/a_url_versioned_ApiController.cs @@ -0,0 +1,71 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.Http.Basic; + using Microsoft.Web.Http.Basic.Controllers; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ApiController : BasicAcceptanceTest + { + [Theory] + [InlineData( "api/v1/helloworld", null )] + [InlineData( "api/v1/helloworld/42", "42" )] + public async Task _get_should_return_200( string requestUrl, string id ) + { + // arrange + var body = new Dictionary() + { + ["controller"] = nameof( HelloWorldController ), + ["version"] = "1" + }; + + if ( !string.IsNullOrEmpty( id ) ) + { + body["id"] = id; + } + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); + content.ShouldBeEquivalentTo( body ); + } + + [Fact] + public async Task _post_should_return_201() + { + // arrange + var entity = default( object ); + + // act + var response = await PostAsync( "api/v1/helloworld", entity ).EnsureSuccessStatusCode(); + + // assert + response.Headers.Location.Should().Be( new Uri( "http://localhost/api/v1/helloworld/42" ) ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/v2/helloworld" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/ByNamespaceAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/ByNamespaceAcceptanceTest.cs new file mode 100644 index 00000000..10724172 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/ByNamespaceAcceptanceTest.cs @@ -0,0 +1,31 @@ +namespace Microsoft.Web.Http.ByNamespace +{ + using Microsoft.Web.Http.Routing; + using System.Web.Http; + using static System.Web.Http.RouteParameter; + + public abstract class ByNamespaceAcceptanceTest : AcceptanceTest + { + protected ByNamespaceAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( Controllers.V1.AgreementsController ) ); + FilteredControllerTypes.Add( typeof( Controllers.V2.AgreementsController ) ); + FilteredControllerTypes.Add( typeof( Controllers.V3.AgreementsController ) ); + + Configuration.AddApiVersioning( options => options.ReportApiVersions = true ); + + Configuration.Routes.MapHttpRoute( + "VersionedQueryString", + "api/{controller}/{accountId}", + new { accountId = Optional } ); + + Configuration.Routes.MapHttpRoute( + "VersionedUrl", + "v{apiVersion}/{controller}/{accountId}", + new { accountId = Optional }, + new { apiVersion = new ApiVersionRouteConstraint() } ); + + Configuration.EnsureInitialized(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V1/AgreementsController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V1/AgreementsController.cs new file mode 100644 index 00000000..4ca0d311 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V1/AgreementsController.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Web.Http.ByNamespace.Controllers.V1 +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + + [ApiVersion( "1.0" )] + public class AgreementsController : ApiController + { + public IHttpActionResult Get( string accountId ) => Ok( new Agreement( GetType().FullName, accountId, Request.GetRequestedApiVersion().ToString() ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V2/AgreementsController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V2/AgreementsController.cs new file mode 100644 index 00000000..efecf2bb --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V2/AgreementsController.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Web.Http.ByNamespace.Controllers.V2 +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + + [ApiVersion( "2.0" )] + public class AgreementsController : ApiController + { + public IHttpActionResult Get( string accountId ) => Ok( new Agreement( GetType().FullName, accountId, Request.GetRequestedApiVersion().ToString() ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V3/AgreementsController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V3/AgreementsController.cs new file mode 100644 index 00000000..406c5a13 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Controllers/V3/AgreementsController.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Web.Http.ByNamespace.Controllers.V3 +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + + [ApiVersion( "3.0" )] + public class AgreementsController : ApiController + { + public IHttpActionResult Get( string accountId ) => Ok( new Agreement( GetType().FullName, accountId, Request.GetRequestedApiVersion().ToString() ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Models/Agreement.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Models/Agreement.cs new file mode 100644 index 00000000..10827d67 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/Models/Agreement.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Web.Http.ByNamespace.Models +{ + using System; + + public class Agreement + { + public Agreement( string controller, string accountId, string apiVersion ) + { + Controller = controller; + AccountId = accountId; + ApiVersion = apiVersion; + } + + public string Controller { get; set; } + + public string AccountId { get; set; } + + public string ApiVersion { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_query_string_versioned_ApiController_per_namespace.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_query_string_versioned_ApiController_per_namespace.cs new file mode 100644 index 00000000..64d40b51 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_query_string_versioned_ApiController_per_namespace.cs @@ -0,0 +1,54 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.Http.ByNamespace; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ApiController_per_namespace : ByNamespaceAcceptanceTest + { + [Theory] + [InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V1.AgreementsController", "1.0" )] + [InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V2.AgreementsController", "2.0" )] + [InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V3.AgreementsController", "3.0" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + + + // act + var response = await GetAsync( $"api/agreements/42?api-version={apiVersion}" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( + new Dictionary() + { + ["Controller"] = controller, + ["ApiVersion"] = apiVersion, + ["AccountId"] = "42" + } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/agreements/42?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_url_versioned_ApiController_per_namespace.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_url_versioned_ApiController_per_namespace.cs new file mode 100644 index 00000000..4b6a1f4c --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/ByNamespace/given/a_url_versioned_ApiController_per_namespace.cs @@ -0,0 +1,54 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.Http.ByNamespace; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ApiController_per_namespace : ByNamespaceAcceptanceTest + { + [Theory] + [InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V1.AgreementsController", "1" )] + [InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V2.AgreementsController", "2" )] + [InlineData( "Microsoft.Web.Http.ByNamespace.Controllers.V3.AgreementsController", "3" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + + + // act + var response = await GetAsync( $"v{apiVersion}/agreements/42" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( + new Dictionary() + { + ["Controller"] = controller, + ["ApiVersion"] = apiVersion, + ["AccountId"] = "42" + } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "v4/agreements/42" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/HelloWorldController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/HelloWorldController.cs new file mode 100644 index 00000000..58af803e --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/HelloWorldController.cs @@ -0,0 +1,14 @@ +namespace Microsoft.Web.Http.Conventions.Controllers +{ + using System.Web.Http; + + [RoutePrefix( "api/v{version:apiVersion}/helloworld" )] + public class HelloWorldController : ApiController + { + [Route] + public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route( "{id:int}" )] + public IHttpActionResult Get( int id ) => Ok( new { controller = GetType().Name, id = id, version = Request.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/Values2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/Values2Controller.cs new file mode 100644 index 00000000..eb23e048 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/Values2Controller.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Web.Http.Conventions.Controllers +{ + using System.Web.Http; + + [RoutePrefix( "api/values" )] + public class Values2Controller : ApiController + { + [Route] + public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route( "{id:int}" )] + public IHttpActionResult Get( int id ) => Ok( new { controller = GetType().Name, id = id, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route] + public IHttpActionResult GetV3() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route( "{id:int}" )] + public IHttpActionResult GetV3( int id ) => Ok( new { controller = GetType().Name, id = id, version = Request.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/ValuesController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/ValuesController.cs new file mode 100644 index 00000000..dd7259f3 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/Controllers/ValuesController.cs @@ -0,0 +1,14 @@ +namespace Microsoft.Web.Http.Conventions.Controllers +{ + using System.Web.Http; + + [RoutePrefix( "api/values" )] + public class ValuesController : ApiController + { + [Route] + public IHttpActionResult Get() => Ok( new { controller = GetType().Name, version = Request.GetRequestedApiVersion().ToString() } ); + + [Route( "{id:int}" )] + public IHttpActionResult Get( int id ) => Ok( new { controller = GetType().Name, id = id, version = Request.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/ConventionsAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/ConventionsAcceptanceTest.cs new file mode 100644 index 00000000..82e07aa7 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/ConventionsAcceptanceTest.cs @@ -0,0 +1,40 @@ +namespace Microsoft.Web.Http.Conventions +{ + using Controllers; + using Microsoft.Web.Http.Routing; + using System.Web.Http; + using System.Web.Http.Routing; + using Versioning.Conventions; + + public abstract class ConventionsAcceptanceTest : AcceptanceTest + { + protected ConventionsAcceptanceTest() + { + var constraintResolver = new DefaultInlineConstraintResolver() + { + ConstraintMap = { ["apiVersion"] = typeof( ApiVersionRouteConstraint ) } + }; + + FilteredControllerTypes.Add( typeof( ValuesController ) ); + FilteredControllerTypes.Add( typeof( Values2Controller ) ); + FilteredControllerTypes.Add( typeof( HelloWorldController ) ); + Configuration.AddApiVersioning( + options => + { + options.ReportApiVersions = true; + options.Conventions.Controller().HasApiVersion( 1, 0 ); + options.Conventions.Controller() + .HasApiVersion( 2, 0 ) + .HasApiVersion( 3, 0 ) + .Action( c => c.GetV3() ).MapToApiVersion( 3, 0 ) + .Action( c => c.GetV3( default( int ) ) ).MapToApiVersion( 3, 0 ); + options.Conventions.Controller() + .HasApiVersion( 1, 0 ) + .HasApiVersion( 2, 0 ) + .AdvertisesApiVersion( 3, 0 ); + } ); + Configuration.MapHttpAttributeRoutes( constraintResolver ); + Configuration.EnsureInitialized(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_query_string_versioned_ApiController_split_into_two_types_using_conventions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_query_string_versioned_ApiController_split_into_two_types_using_conventions.cs new file mode 100644 index 00000000..4feb9875 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_query_string_versioned_ApiController_split_into_two_types_using_conventions.cs @@ -0,0 +1,54 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.Http.Conventions; + using Microsoft.Web.Http.Conventions.Controllers; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ApiController_split_into_two_types_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( nameof( ValuesController ), "1.0" )] + [InlineData( nameof( Values2Controller ), "2.0" )] + [InlineData( nameof( Values2Controller ), "3.0" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + + + // act + var response = await GetAsync( $"api/values?api-version={apiVersion}" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( + new Dictionary() + { + ["controller"] = controller, + ["version"] = apiVersion + } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/values?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_url_versioned_ApiController_using_conventions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_url_versioned_ApiController_using_conventions.cs new file mode 100644 index 00000000..9392e967 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Http/Conventions/given/a_url_versioned_ApiController_using_conventions.cs @@ -0,0 +1,59 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.Http.Conventions; + using Microsoft.Web.Http.Conventions.Controllers; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ApiController_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( "api/v1/helloworld", "1", null )] + [InlineData( "api/v2/helloworld", "2", null )] + [InlineData( "api/v1/helloworld/42", "1", "42" )] + [InlineData( "api/v2/helloworld/42", "2", "42" )] + public async Task _get_should_return_200( string requestUrl, string apiVersion, string id ) + { + // arrange + var body = new Dictionary() + { + ["controller"] = nameof( HelloWorldController ), + ["version"] = apiVersion + }; + + if ( !string.IsNullOrEmpty( id ) ) + { + body["id"] = id; + } + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( body ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/v3/helloworld" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpContentExtensions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpContentExtensions.cs new file mode 100644 index 00000000..fde39c9c --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpContentExtensions.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Web +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + + internal static class HttpContentExtensions + { + internal static Task ReadAsExampleAsync( this HttpContent content, T example ) => content.ReadAsAsync(); + } +} diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpSimulatorHandler.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpSimulatorHandler.cs new file mode 100644 index 00000000..c9027d86 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/HttpSimulatorHandler.cs @@ -0,0 +1,74 @@ +namespace Microsoft.Web +{ + using System.Diagnostics.Contracts; + using System.IO; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading; + using System.Threading.Tasks; + + internal sealed class HttpSimulatorHandler : DelegatingHandler + { + internal HttpSimulatorHandler( HttpMessageHandler innerHandler ) + : base( innerHandler ) + { + } + + protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) + { + request = await SimulateRequestOverTheWireAsync( request, cancellationToken ); + var response = await base.SendAsync( request, cancellationToken ); + return await SimulateResponseOverTheWireAsync( response, cancellationToken ); + } + + private async Task SimulateRequestOverTheWireAsync( HttpRequestMessage request, CancellationToken cancellationToken ) + { + Contract.Requires( request != null ); + Contract.Requires( Contract.Result>() != null ); + + var stream = new MemoryStream(); + HttpContent content = new HttpMessageContent( request ); + + await content.CopyToAsync( stream ); + await stream.FlushAsync( cancellationToken ); + + stream.Position = 0L; + content = new StreamContent( stream ); + SetMediaType( content, "request" ); + + return await content.ReadAsHttpRequestMessageAsync( cancellationToken ); + } + + private async Task SimulateResponseOverTheWireAsync( HttpResponseMessage response, CancellationToken cancellationToken ) + { + Contract.Requires( response != null ); + Contract.Requires( Contract.Result>() != null ); + + var stream = new MemoryStream(); + HttpContent content = new HttpMessageContent( response ); + + await content.CopyToAsync( stream ); + await stream.FlushAsync( cancellationToken ); + + stream.Position = 0L; + content = new StreamContent( stream ); + SetMediaType( content, "response" ); + + return await content.ReadAsHttpResponseMessageAsync( cancellationToken ); + } + + private static void SetMediaType( HttpContent content, string messageType ) + { + Contract.Requires( content != null ); + Contract.Requires( !string.IsNullOrEmpty( messageType ) ); + + content.Headers.ContentType = new MediaTypeHeaderValue( "application/http" ) + { + Parameters = + { + new NameValueHeaderValue( "msgtype", messageType ) + } + }; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Microsoft.AspNet.WebApi.Acceptance.Tests.xproj b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Microsoft.AspNet.WebApi.Acceptance.Tests.xproj new file mode 100644 index 00000000..36934656 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Microsoft.AspNet.WebApi.Acceptance.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 5c31964d-ea8b-420b-9297-5adfefe54962 + Microsoft.Web + .\obj + .\bin\ + v4.5 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedAcceptanceTest.cs new file mode 100644 index 00000000..0b047ae3 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/AdvancedAcceptanceTest.cs @@ -0,0 +1,54 @@ +namespace Microsoft.Web.OData.Advanced +{ + using Builder; + using Configuration; + using Controllers; + using Http; + using Http.Versioning; + using System.Web.Http; + using System.Web.OData.Builder; + using static System.Web.Http.RouteParameter; + + public abstract class AdvancedAcceptanceTest : ODataAcceptanceTest + { + protected AdvancedAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( OrdersController ) ); + FilteredControllerTypes.Add( typeof( Orders2Controller ) ); + FilteredControllerTypes.Add( typeof( Orders3Controller ) ); + FilteredControllerTypes.Add( typeof( PeopleController ) ); + FilteredControllerTypes.Add( typeof( People2Controller ) ); + + Configuration.MapHttpAttributeRoutes(); + Configuration.AddApiVersioning( + options => + { + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + options.ApiVersionReader = new QueryStringOrHeaderApiVersionReader() + { + HeaderNames = + { + "api-version", + "x-ms-version" + } + }; + } ); + + var modelBuilder = new VersionedODataModelBuilder( Configuration ) + { + ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(), + ModelConfigurations = + { + new PersonModelConfiguration(), + new OrderModelConfiguration( supportedApiVersion: new ApiVersion( 2, 0 ) ) + } + }; + var models = modelBuilder.GetEdmModels(); + + Configuration.MapVersionedODataRoutes( "odata", "api", models ); + Configuration.Routes.MapHttpRoute( "orders", "api/{controller}/{key}", new { key = Optional } ); + Configuration.EnsureInitialized(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs new file mode 100644 index 00000000..9dff86c5 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders2Controller.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Web.OData.Advanced.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ApiVersion( "2.0" )] + [ControllerName( "Orders" )] + [ODataRoutePrefix( "Orders" )] + public class Orders2Controller : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Order() { Id = key, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs new file mode 100644 index 00000000..ba75fe65 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/Orders3Controller.cs @@ -0,0 +1,18 @@ +namespace Microsoft.Web.OData.Advanced.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + + [ApiVersion( "3.0" )] + [ControllerName( "Orders" )] + [RoutePrefix( "api/orders" )] + public class Orders3Controller : ApiController + { + [Route] + public IHttpActionResult Get() => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); + + [Route( "{key}" )] + public IHttpActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs new file mode 100644 index 00000000..f1fe1abe --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/OrdersController.cs @@ -0,0 +1,12 @@ +namespace Microsoft.Web.OData.Advanced.Controllers +{ + using Models; + using System.Web.Http; + + public class OrdersController : ApiController + { + public IHttpActionResult Get() => Ok( new[] { new Order() { Id = 1, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } } ); + + public IHttpActionResult Get( int key ) => Ok( new Order() { Id = key, Customer = $"Customer v{Request.GetRequestedApiVersion()}" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs new file mode 100644 index 00000000..926a5030 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/People2Controller.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Web.OData.Advanced.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ApiVersion( "3.0" )] + [ControllerName( "People" )] + [ODataRoutePrefix( "People" )] + public class People2Controller : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs new file mode 100644 index 00000000..5b2ef9a2 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/Controllers/PeopleController.cs @@ -0,0 +1,39 @@ +namespace Microsoft.Web.OData.Advanced.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + [ODataRoutePrefix( "People" )] + public class PeopleController : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + [MapToApiVersion( "2.0" )] + [ODataRoute( "({key})" )] + public IHttpActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_mix-in_people_ODataController_split_into_two_types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_mix-in_people_ODataController_split_into_two_types.cs new file mode 100644 index 00000000..93cbcf96 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_mix-in_people_ODataController_split_into_two_types.cs @@ -0,0 +1,158 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Advanced; + using System; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_mixX2Din_people_ODataController_split_into_two_types : AdvancedAcceptanceTest + { + [Theory] + [InlineData( "api/people" )] + [InlineData( "api/people?api-version=1.0" )] + public async Task _get_should_return_200_for_v1( string requestUrl ) + { + // arrange + var example = new { value = new[] { new { id = 0, firstName = "", lastName = "" } } }; + + // act + var response = await Client.GetAsync( requestUrl ); + var people = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); + + // assert + people.value.ShouldBeEquivalentTo( + new[] { new { id = 1, firstName = "Bill", lastName = "Mei" } }, + options => options.ExcludingMissingMembers() ); + } + + [Theory] + [InlineData( "api/people(42)" )] + [InlineData( "api/people(42)?api-version=1.0" )] + public async Task _get_with_key_should_return_200_for_v1( string requestUrl ) + { + // arrange + var example = new { id = 0, firstName = "", lastName = "" }; + + // act + var response = await Client.GetAsync( requestUrl ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); + + // assert + order.ShouldBeEquivalentTo( + new { id = 42, firstName = "Bill", lastName = "Mei" }, + options => options.ExcludingMissingMembers() ); + } + + [Fact] + public async Task _get_should_return_200_for_v2() + { + // arrange + var example = new { value = new[] { new { id = 0, firstName = "", lastName = "", email = "" } } }; + + // act + var response = await Client.GetAsync( "api/people?api-version=2.0" ); + var people = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); + + // assert + people.value.ShouldBeEquivalentTo( + new[] { new { id = 1, firstName = "Bill", lastName = "Mei", email = "bill.mei@somewhere.com" } }, + options => options.ExcludingMissingMembers() ); + } + + [Fact] + public async Task _get_with_key_should_return_200_for_v2() + { + // arrange + var example = new { id = 0, firstName = "", lastName = "", email = "" }; + + // act + var response = await Client.GetAsync( "api/people(42)?api-version=2.0" ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); + + // assert + order.ShouldBeEquivalentTo( + new { id = 42, firstName = "Bill", lastName = "Mei", email = "bill.mei@somewhere.com" }, + options => options.ExcludingMissingMembers() ); + } + + [Fact] + public async Task _get_should_return_200_for_v3() + { + // arrange + var example = new { value = new[] { new { id = 0, firstName = "", lastName = "", email = "", phone = "" } } }; + + // act + var response = await Client.GetAsync( "api/people?api-version=3.0" ); + var people = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); + + // assert + people.value.ShouldBeEquivalentTo( + new[] { new { id = 1, firstName = "Bill", lastName = "Mei", email = "bill.mei@somewhere.com", phone = "555-555-5555" } }, + options => options.ExcludingMissingMembers() ); + } + + [Fact] + public async Task _get_with_key_should_return_200_for_v3() + { + // arrange + var example = new { id = 0, firstName = "", lastName = "", email = "", phone = "" }; + + // act + var response = await Client.GetAsync( "api/people(42)?api-version=3.0" ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( example ); + + // assert + order.ShouldBeEquivalentTo( + new { id = 42, firstName = "Bill", lastName = "Mei", email = "bill.mei@somewhere.com", phone = "555-555-5555" }, + options => options.ExcludingMissingMembers() ); + } + + [Fact] + public async Task _patch_should_return_204() + { + // arrange + var person = new { email = "bmei@somewhere.com" }; + + // act + var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + + // assert + response.StatusCode.Should().Be( NoContent ); + } + + [Fact] + public async Task _patch_should_return_400_when_updating_member_that_does_not_exist_in_api_version() + { + // arrange + var person = new { phone = "bmei@somewhere.com" }; + + // act + var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + + // assert + response.StatusCode.Should().Be( BadRequest ); + } + + [Theory] + [InlineData( "1.0" )] + [InlineData( "3.0" )] + [InlineData( "4.0" )] + public async Task _patch_should_return_400_when_version_is_unsupported( string apiVersion ) + { + // arrange + var person = new { lastName = "Me" }; + + // act + var response = await PatchAsync( $"api/people(42)?api-version={apiVersion}", person ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v1_orders_ApiController_using_convention_routing.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v1_orders_ApiController_using_convention_routing.cs new file mode 100644 index 00000000..590e673d --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v1_orders_ApiController_using_convention_routing.cs @@ -0,0 +1,68 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Advanced; + using System; + using System.Threading.Tasks; + using Xunit; + + public class _a_v1_orders_ApiController_using_convention_routing : AdvancedAcceptanceTest + { + [Fact] + public async Task _get_should_return_200_without_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders" ); + var orders = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new[] { new { Id = 0, Customer = "" } } ); + + // assert + orders.ShouldBeEquivalentTo( new[] { new { Id = 1, Customer = "Customer v1.0" } } ); + } + + [Fact] + public async Task _get_should_return_200() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders?api-version=1.0" ); + var orders = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new[] { new { Id = 0, Customer = "" } } ); + + // assert + orders.ShouldBeEquivalentTo( new[] { new { Id = 1, Customer = "Customer v1.0" } } ); + } + + [Fact] + public async Task _get_with_key_should_return_200_without_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders/42" ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { Id = 0, Customer = "" } ); + + // assert + order.ShouldBeEquivalentTo( new { Id = 42, Customer = "Customer v1.0" } ); + } + + [Fact] + public async Task _get_with_key_should_return_200() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders/42?api-version=1.0" ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { Id = 0, Customer = "" } ); + + // assert + order.ShouldBeEquivalentTo( new { Id = 42, Customer = "Customer v1.0" } ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v2_orders_ODataController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v2_orders_ODataController.cs new file mode 100644 index 00000000..257ec780 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v2_orders_ODataController.cs @@ -0,0 +1,40 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Advanced; + using System; + using System.Threading.Tasks; + using Xunit; + + public class _a_v2_orders_ODataController : AdvancedAcceptanceTest + { + [Fact] + public async Task _get_should_return_200() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders?api-version=2.0" ); + var orders = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { value = new[] { new { id = 0, customer = "" } } } ); + + // assert + orders.value.ShouldBeEquivalentTo( new[] { new { id = 1, customer = "Customer v2.0" } }, options => options.ExcludingMissingMembers() ); + } + + [Fact] + public async Task _get_with_key_should_return_200() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders(42)?api-version=2.0" ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { id = 0, customer = "" } ); + + // assert + order.ShouldBeEquivalentTo( new { id = 42, customer = "Customer v2.0" }, options => options.ExcludingMissingMembers() ); + } + } +} diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v3_orders_ApiController_using_attribute_routing.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v3_orders_ApiController_using_attribute_routing.cs new file mode 100644 index 00000000..fd5603a3 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Advanced/given/a_v3_orders_ApiController_using_attribute_routing.cs @@ -0,0 +1,40 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Advanced; + using System; + using System.Threading.Tasks; + using Xunit; + + public class _a_v3_orders_ApiController_using_attribute_routing : AdvancedAcceptanceTest + { + [Fact] + public async Task _get_should_return_200() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders?api-version=3.0" ); + var orders = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new[] { new { Id = 0, Customer = "" } } ); + + // assert + orders.ShouldBeEquivalentTo( new[] { new { Id = 1, Customer = "Customer v3.0" } } ); + } + + [Fact] + public async Task _get_with_key_should_return_200_without_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/orders/42?api-version=3.0" ); + var order = await response.EnsureSuccessStatusCode().Content.ReadAsExampleAsync( new { Id = 0, Customer = "" } ); + + // assert + order.ShouldBeEquivalentTo( new { Id = 42, Customer = "Customer v3.0" } ); + } + } +} diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs new file mode 100644 index 00000000..01d1fd94 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/BasicAcceptanceTest.cs @@ -0,0 +1,69 @@ +namespace Microsoft.Web.OData.Basic +{ + using Builder; + using Configuration; + using Controllers; + using FluentAssertions; + using System.Net.Http; + using System.Threading.Tasks; + using System.Web.Http; + using System.Web.OData.Builder; + using Xunit; + using static System.Net.HttpStatusCode; + + public abstract class BasicAcceptanceTest : ODataAcceptanceTest + { + protected BasicAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( OrdersController ) ); + FilteredControllerTypes.Add( typeof( PeopleController ) ); + FilteredControllerTypes.Add( typeof( People2Controller ) ); + + Configuration.AddApiVersioning( options => options.ReportApiVersions = true ); + + var modelBuilder = new VersionedODataModelBuilder( Configuration ) + { + ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(), + ModelConfigurations = + { + new PersonModelConfiguration(), + new OrderModelConfiguration() + } + }; + var models = modelBuilder.GetEdmModels(); + + Configuration.MapVersionedODataRoutes( "odata", "api", models ); + Configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models ); + Configuration.EnsureInitialized(); + } + + [Fact] + public async Task _service_document_should_return_400_for_unsupported_url_api_version() + { + // arrange + var requestUrl = $"v4"; + + // act + var response = await Client.GetAsync( requestUrl ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + + [Fact] + public async Task _metadata_should_return_400_for_unsupported_url_api_version() + { + // arrange + + // act + var response = await Client.GetAsync( "v4/$metadata" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs new file mode 100644 index 00000000..8e711304 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/OrdersController.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Web.OData.Basic.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ApiVersion( "1.0" )] + [ODataRoutePrefix( "Orders" )] + public class OrdersController : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Order() { Id = key, Customer = "Bill Mei" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs new file mode 100644 index 00000000..37a81fff --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/People2Controller.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Web.OData.Basic.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ApiVersion( "3.0" )] + [ControllerName( "People" )] + [ODataRoutePrefix( "People" )] + public class People2Controller : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs new file mode 100644 index 00000000..9f9a37a6 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/Controllers/PeopleController.cs @@ -0,0 +1,39 @@ +namespace Microsoft.Web.OData.Basic.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ApiVersion( "1.0" )] + [ApiVersion( "2.0" )] + [ODataRoutePrefix( "People" )] + public class PeopleController : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + [MapToApiVersion( "2.0" )] + [ODataRoute( "({key})" )] + public IHttpActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController.cs new file mode 100644 index 00000000..cb0396d1 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController.cs @@ -0,0 +1,44 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Basic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ODataController : BasicAcceptanceTest + { + [Theory] + [InlineData( "api/orders?api-version=1.0" )] + [InlineData( "api/orders(42)?api-version=1.0" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/orders?api-version=2.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController_split_into_two_types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController_split_into_two_types.cs new file mode 100644 index 00000000..0e45d179 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_query_string_versioned_ODataController_split_into_two_types.cs @@ -0,0 +1,79 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Basic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ODataController_split_into_two_types : BasicAcceptanceTest + { + [Theory] + [InlineData( "api/people?api-version=1.0" )] + [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people?api-version=2.0" )] + [InlineData( "api/people(42)?api-version=2.0" )] + [InlineData( "api/people?api-version=3.0" )] + [InlineData( "api/people(42)?api-version=3.0" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/people?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + + [Fact] + public async Task _patch_should_return_204() + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + + // assert + response.StatusCode.Should().Be( NoContent ); + } + + [Theory] + [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people(42)?api-version=4.0" )] + public async Task _patch_should_return_400_when_version_is_unsupported( string requestUrl ) + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( requestUrl, person ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController.cs new file mode 100644 index 00000000..ed7f5754 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController.cs @@ -0,0 +1,44 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Basic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ODataController : BasicAcceptanceTest + { + [Theory] + [InlineData( "v1/orders" )] + [InlineData( "v1/orders(42)" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "v2/orders" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController_split_into_two_types.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController_split_into_two_types.cs new file mode 100644 index 00000000..a38c54a5 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Basic/given/a_url_versioned_ODataController_split_into_two_types.cs @@ -0,0 +1,64 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Basic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ODataController_split_into_two_types : BasicAcceptanceTest + { + [Theory] + [InlineData( "v1/people" )] + [InlineData( "v1/people(42)" )] + [InlineData( "v2/people" )] + [InlineData( "v2/people(42)" )] + [InlineData( "v3/people" )] + [InlineData( "v3/people(42)" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + } + + [Fact] + public async Task _patch_should_return_204() + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( "v2/people(42)", person ); + + // assert + response.StatusCode.Should().Be( NoContent ); + } + + [Theory] + [InlineData( "v1/people(42)" )] + [InlineData( "v3/people(42)" )] + [InlineData( "v4/people(42)" )] + public async Task _patch_should_return_400_when_version_is_unsupported( string requestUrl ) + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( requestUrl, person ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs new file mode 100644 index 00000000..a9d1be97 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs @@ -0,0 +1,39 @@ +namespace Microsoft.Web.OData.Configuration +{ + using Builder; + using Http; + using Models; + using System.Web.OData.Builder; + + public class OrderModelConfiguration : IModelConfiguration + { + private static readonly ApiVersion V1 = new ApiVersion( 1, 0 ); + private readonly ApiVersion supportedApiVersion; + + public OrderModelConfiguration() : this( V1 ) + { + } + + public OrderModelConfiguration( ApiVersion supportedApiVersion ) + { + this.supportedApiVersion = supportedApiVersion; + } + + private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + { + var order = builder.EntitySet( "Orders" ).EntityType; + + order.HasKey( p => p.Id ); + + return order; + } + + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + { + if ( supportedApiVersion == apiVersion ) + { + ConfigureCurrent( builder ); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs new file mode 100644 index 00000000..a4a70ec0 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs @@ -0,0 +1,44 @@ +namespace Microsoft.Web.OData.Configuration +{ + using Microsoft.Web.Http; + using Microsoft.Web.OData.Builder; + using Models; + using System.Web.OData.Builder; + + public class PersonModelConfiguration : IModelConfiguration + { + private void ConfigureV1( ODataModelBuilder builder ) + { + var person = ConfigureCurrent( builder ); + person.Ignore( p => p.Email ); + person.Ignore( p => p.Phone ); + } + + private void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); + + private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + { + var person = builder.EntitySet( "People" ).EntityType; + + person.HasKey( p => p.Id ); + + return person; + } + + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion ) + { + switch ( apiVersion.MajorVersion ) + { + case 1: + ConfigureV1( builder ); + break; + case 2: + ConfigureV2( builder ); + break; + default: + ConfigureCurrent( builder ); + break; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs new file mode 100644 index 00000000..424acf07 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/OrdersController.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Web.OData.Conventions.Controllers +{ + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ODataRoutePrefix( "Orders" )] + public class OrdersController : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Order() { Id = 1, Customer = "Bill Mei" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Order() { Id = key, Customer = "Bill Mei" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs new file mode 100644 index 00000000..b8946623 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/People2Controller.cs @@ -0,0 +1,22 @@ +namespace Microsoft.Web.OData.Conventions.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ControllerName( "People" )] + [ODataRoutePrefix( "People" )] + public class People2Controller : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs new file mode 100644 index 00000000..c0d130eb --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/Controllers/PeopleController.cs @@ -0,0 +1,37 @@ +namespace Microsoft.Web.OData.Conventions.Controllers +{ + using Microsoft.Web.Http; + using Models; + using System.Web.Http; + using System.Web.OData; + using System.Web.OData.Query; + using System.Web.OData.Routing; + + [ODataRoutePrefix( "People" )] + public class PeopleController : ODataController + { + [ODataRoute] + public IHttpActionResult Get( ODataQueryOptions options ) => + Ok( new[] { new Person() { Id = 1, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } } ); + + [ODataRoute( "({key})" )] + public IHttpActionResult Get( [FromODataUri] int key, ODataQueryOptions options ) => + Ok( new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" } ); + + [MapToApiVersion( "2.0" )] + [ODataRoute( "({key})" )] + public IHttpActionResult Patch( [FromODataUri] int key, Delta delta, ODataQueryOptions options ) + { + if ( !ModelState.IsValid ) + { + return BadRequest( ModelState ); + } + + var person = new Person() { Id = key, FirstName = "Bill", LastName = "Mei", Email = "bill.mei@somewhere.com", Phone = "555-555-5555" }; + + delta.Patch( person ); + + return Updated( person ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsAcceptanceTest.cs new file mode 100644 index 00000000..7298a994 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/ConventionsAcceptanceTest.cs @@ -0,0 +1,49 @@ +namespace Microsoft.Web.OData.Conventions +{ + using Builder; + using Configuration; + using Controllers; + using Http.Versioning.Conventions; + using System.Web.Http; + using System.Web.OData.Builder; + using System.Web.OData.Extensions; + + public abstract class ConventionsAcceptanceTest : ODataAcceptanceTest + { + protected ConventionsAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( OrdersController ) ); + FilteredControllerTypes.Add( typeof( PeopleController ) ); + FilteredControllerTypes.Add( typeof( People2Controller ) ); + + Configuration.AddApiVersioning( + options => + { + options.ReportApiVersions = true; + options.Conventions.Controller() + .HasApiVersion( 1, 0 ); + options.Conventions.Controller() + .HasApiVersion( 1, 0 ) + .HasApiVersion( 2, 0 ) + .Action( c => c.Patch( default( int ), null, null ) ).MapToApiVersion( 2, 0 ); + options.Conventions.Controller() + .HasApiVersion( 3, 0 ); + } ); + + var modelBuilder = new VersionedODataModelBuilder( Configuration ) + { + ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase(), + ModelConfigurations = + { + new PersonModelConfiguration(), + new OrderModelConfiguration() + } + }; + var models = modelBuilder.GetEdmModels(); + + Configuration.MapVersionedODataRoutes( "odata", "api", models ); + Configuration.MapVersionedODataRoutes( "odata-bypath", "v{apiVersion}", models ); + Configuration.EnsureInitialized(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_split_into_two_types_using_conventions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_split_into_two_types_using_conventions.cs new file mode 100644 index 00000000..7346c91a --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_split_into_two_types_using_conventions.cs @@ -0,0 +1,79 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Conventions; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ODataController_split_into_two_types_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( "api/people?api-version=1.0" )] + [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people?api-version=2.0" )] + [InlineData( "api/people(42)?api-version=2.0" )] + [InlineData( "api/people?api-version=3.0" )] + [InlineData( "api/people(42)?api-version=3.0" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/people?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + + [Fact] + public async Task _patch_should_return_204() + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( "api/people(42)?api-version=2.0", person ); + + // assert + response.StatusCode.Should().Be( NoContent ); + } + + [Theory] + [InlineData( "api/people(42)?api-version=1.0" )] + [InlineData( "api/people(42)?api-version=3.0" )] + [InlineData( "api/people(42)?api-version=4.0" )] + public async Task _patch_should_return_400_when_version_is_unsupported( string requestUrl ) + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( requestUrl, person ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_using_conventions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_using_conventions.cs new file mode 100644 index 00000000..bfbb63c7 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_query_string_versioned_ODataController_using_conventions.cs @@ -0,0 +1,44 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Conventions; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_ODataController_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( "api/orders?api-version=1.0" )] + [InlineData( "api/orders(42)?api-version=1.0" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/orders?api-version=2.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_split_into_two_types_using_conventions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_split_into_two_types_using_conventions.cs new file mode 100644 index 00000000..fd57cfee --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_split_into_two_types_using_conventions.cs @@ -0,0 +1,64 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Conventions; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ODataController_split_into_two_types_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( "v1/people" )] + [InlineData( "v1/people(42)" )] + [InlineData( "v2/people" )] + [InlineData( "v2/people(42)" )] + [InlineData( "v3/people" )] + [InlineData( "v3/people(42)" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + } + + [Fact] + public async Task _patch_should_return_204() + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( "v2/people(42)", person ); + + // assert + response.StatusCode.Should().Be( NoContent ); + } + + [Theory] + [InlineData( "v1/people(42)" )] + [InlineData( "v3/people(42)" )] + [InlineData( "v4/people(42)" )] + public async Task _patch_should_return_400_when_version_is_unsupported( string requestUrl ) + { + // arrange + var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; + + // act + var response = await PatchAsync( requestUrl, person ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_using_conventions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_using_conventions.cs new file mode 100644 index 00000000..c6fde96e --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Conventions/given/a_url_versioned_ODataController_using_conventions.cs @@ -0,0 +1,44 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.Web; + using Microsoft.Web.OData.Conventions; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_ODataController_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( "v1/orders" )] + [InlineData( "v1/orders(42)" )] + public async Task _get_should_return_200( string requestUrl ) + { + // arrange + + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "v2/orders" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Order.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Order.cs new file mode 100644 index 00000000..b637cdeb --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Order.cs @@ -0,0 +1,20 @@ +namespace Microsoft.Web.OData.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Web; + + public class Order + { + public int Id { get; set; } + + public DateTimeOffset CreatedDate { get; set; } = DateTimeOffset.Now; + + public DateTimeOffset EffectiveDate { get; set; } = DateTimeOffset.Now; + + [Required] + public string Customer { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Person.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Person.cs new file mode 100644 index 00000000..d5f3e227 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/Models/Person.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Web.OData.Models +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public class Person + { + public int Id { get; set; } + + [Required] + [StringLength( 25 )] + public string FirstName { get; set; } + + [Required] + [StringLength( 25 )] + public string LastName { get; set; } + + public string Email { get; set; } + + public string Phone { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs new file mode 100644 index 00000000..748eca2e --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs @@ -0,0 +1,108 @@ +namespace Microsoft.Web.OData +{ + using FluentAssertions; + using OData.Controllers; + using System.Net.Http; + using System.Threading.Tasks; + using System.Web.OData.Extensions; + using Xunit; + using static System.Net.HttpStatusCode; + + public abstract class ODataAcceptanceTest : AcceptanceTest + { + protected ODataAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( VersionedMetadataController ) ); + Configuration.EnableCaseInsensitive( true ); + Configuration.EnableUnqualifiedNameCall( true ); + } + + [Fact] + public async Task _service_document_should_return_result_without_api_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api" ); + + // assert + response.StatusCode.Should().Be( OK ); + } + + [Theory] + [InlineData( "1.0" )] + [InlineData( "2.0" )] + [InlineData( "3.0" )] + public async Task _service_document_should_return_api_version_specific_result( string apiVersion ) + { + // arrange + var requestUrl = $"api?api-version={apiVersion}"; + + // act + var response = await Client.GetAsync( requestUrl ); + + // assert + response.StatusCode.Should().Be( OK ); + } + + [Fact] + public async Task _service_document_should_return_400_for_unsupported_query_string_api_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + + [Fact] + public async Task _metadata_should_return_result_without_api_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/$metadata" ); + + // assert + response.StatusCode.Should().Be( OK ); + } + + [Theory] + [InlineData( "1.0" )] + [InlineData( "2.0" )] + [InlineData( "3.0" )] + public async Task _metadata_should_return_api_version_specific_result( string apiVersion ) + { + // arrange + var requestUrl = $"api/$metadata?api-version={apiVersion}"; + + // act + var response = await Client.GetAsync( requestUrl ); + + // assert + response.StatusCode.Should().Be( OK ); + } + + [Fact] + public async Task _metadata_should_return_400_for_unsupported_query_string_api_version() + { + // arrange + + + // act + var response = await Client.GetAsync( "api/$metadata?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiError.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiError.cs new file mode 100644 index 00000000..be7bcd26 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiError.cs @@ -0,0 +1,13 @@ +namespace Microsoft.Web +{ + using System; + + public class OneApiError + { + public string Code { get; set; } + + public string Message { get; set; } + + public OneApiInnerError InnerError { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiErrorResponse.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiErrorResponse.cs new file mode 100644 index 00000000..02086950 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiErrorResponse.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Web +{ + using System; + + public class OneApiErrorResponse + { + public OneApiError Error { get; set; } + } +} diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiInnerError.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiInnerError.cs new file mode 100644 index 00000000..b56dddd3 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/OneApiInnerError.cs @@ -0,0 +1,11 @@ +namespace Microsoft.Web +{ + using System; + + public class OneApiInnerError + { + public string Code { get; set; } + + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..777decd8 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.AspNet.WebApi.Acceptance.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5c31964d-ea8b-420b-9297-5adfefe54962")] diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TaskExtensions.cs b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TaskExtensions.cs new file mode 100644 index 00000000..f3d07f69 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/TaskExtensions.cs @@ -0,0 +1,16 @@ +namespace Microsoft.Web +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + + internal static class TaskExtensions + { + internal static async Task EnsureSuccessStatusCode( this Task task ) + { + var response = await task; + response.EnsureSuccessStatusCode(); + return response; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/app.config b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/app.config new file mode 100644 index 00000000..c286bd1b --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/app.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/Microsoft.AspNet.WebApi.Acceptance.Tests/project.json b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/project.json new file mode 100644 index 00000000..c1ebc873 --- /dev/null +++ b/test/Microsoft.AspNet.WebApi.Acceptance.Tests/project.json @@ -0,0 +1,32 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + "Microsoft.AspNet.OData": "5.9.1", + "FluentAssertions": "4.12.0", + "more.xunit": "2.1.0", + "xunit.runner.visualstudio": "2.1.0", + "Moq": "4.5.16", + "Microsoft.AspNet.WebApi.Core": "5.2.3", + "Microsoft.AspNet.WebApi.Versioning": { + "target": "project", + "version": "" + }, + "Microsoft.AspNet.OData.Versioning": { + "target": "project", + "version": "" + } + }, + + "frameworks": { + "net45": { + "dependencies": { + "System.Runtime": "4.0.0", + "System.Threading.Tasks": "4.0.0" + }, + "frameworkAssemblies": { + "System.ComponentModel.DataAnnotations": "4.0.0.0" + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/AcceptanceTest.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/AcceptanceTest.cs new file mode 100644 index 00000000..cdddeca6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/AcceptanceTest.cs @@ -0,0 +1,137 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using ApplicationParts; + using AspNetCore.Routing; + using Builder; + using Extensions.DependencyInjection; + using Hosting; + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Net.Http.Formatting; + using System.Net.Http.Headers; + using System.Reflection; + using System.Threading.Tasks; + using TestHost; + using Versioning; + using Xunit; + using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; + using static System.Net.Http.HttpMethod; + + [Trait( "Kind", "Acceptance" )] + public abstract class AcceptanceTest + { + private const string JsonMediaType = "application/json"; + private static readonly HttpMethod Patch = new HttpMethod( "PATCH" ); + private readonly Lazy server; + private readonly Lazy client; + private readonly FilteredControllerFeatureProvider filteredControllerTypes = new FilteredControllerFeatureProvider(); + private bool disposed; + + ~AcceptanceTest() + { + Dispose( false ); + } + + protected AcceptanceTest() + { + server = new Lazy( CreateServer ); + client = new Lazy( CreateAndInitializeHttpClient ); + } + + protected TestServer Server => server.Value; + + protected HttpClient Client => client.Value; + + protected ICollection FilteredControllerTypes => filteredControllerTypes; + + protected virtual void Dispose( bool disposing ) + { + if ( disposed ) + { + return; + } + + disposed = true; + + if ( !disposing ) + { + return; + } + + if ( client.IsValueCreated ) + { + client.Value.Dispose(); + } + + if ( server.IsValueCreated ) + { + server.Value.Dispose(); + } + } + + public void Dispose() + { + Dispose( true ); + GC.SuppressFinalize( this ); + } + + private TestServer CreateServer() + { + var builder = new WebHostBuilder() + .Configure( app => app.UseMvc( OnConfigureRoutes ) ) + .ConfigureServices( OnConfigureServices ); + + return new TestServer( builder ); + } + + private HttpClient CreateAndInitializeHttpClient() + { + var newClient = Server.CreateClient(); + newClient.BaseAddress = new Uri( "http://localhost" ); + return newClient; + } + + private void OnConfigureServices( IServiceCollection services ) + { + var partManager = new ApplicationPartManager(); + + partManager.FeatureProviders.Add( filteredControllerTypes ); + partManager.ApplicationParts.Add( new AssemblyPart( GetType().GetTypeInfo().Assembly ) ); + services.Add( Singleton( partManager ) ); + services.AddMvc(); + services.AddApiVersioning( OnAddApiVersioning ); + } + + protected abstract void OnAddApiVersioning( ApiVersioningOptions options ); + + protected virtual void OnConfigureRoutes( IRouteBuilder routeBuilder ) + { + } + + private HttpRequestMessage CreateRequest( string requestUri, TEntity entity, HttpMethod method ) + { + var request = new HttpRequestMessage( method, requestUri ); + + if ( !Equals( entity, default( TEntity ) ) ) + { + var formatter = new JsonMediaTypeFormatter(); + request.Content = new ObjectContent( entity, formatter, JsonMediaType ); + } + + Client.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue( JsonMediaType ) ); + + return request; + } + + protected virtual Task GetAsync( string requestUri ) => Client.SendAsync( CreateRequest( requestUri, default( object ), Get ) ); + + protected virtual Task PostAsync( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Post ) ); + + protected virtual Task PutAsync( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Put ) ); + + protected virtual Task PatchAsync( string requestUri, TEntity entity ) => Client.SendAsync( CreateRequest( requestUri, entity, Patch ) ); + + protected virtual Task DeleteAsync( string requestUri ) => Client.SendAsync( CreateRequest( requestUri, default( object ), Delete ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/BasicAcceptanceTest.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/BasicAcceptanceTest.cs new file mode 100644 index 00000000..f6bff321 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/BasicAcceptanceTest.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.Mvc.Basic +{ + using Controllers; + using System.Reflection; + using Versioning; + + public abstract class BasicAcceptanceTest: AcceptanceTest + { + protected BasicAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( ValuesController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( Values2Controller ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( HelloWorldController ).GetTypeInfo() ); + } + + protected override void OnAddApiVersioning( ApiVersioningOptions options ) => options.ReportApiVersions = true; + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/HelloWorldController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/HelloWorldController.cs new file mode 100644 index 00000000..f8dbe257 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/HelloWorldController.cs @@ -0,0 +1,19 @@ +namespace Microsoft.AspNetCore.Mvc.Basic.Controllers +{ + using AspNetCore.Routing; + using Microsoft.AspNetCore.Mvc; + using System; + + [Route( "api/v{version:apiVersion}/[controller]" )] + public class HelloWorldController : Controller + { + [HttpGet] + public IActionResult Get() => Ok( new { Controller = nameof( HelloWorldController ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpGet( "{id:int}", Name = "GetMessageById" )] + public IActionResult Get( int id ) => Ok( new { Controller = GetType().Name, Id = id, Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpPost] + public IActionResult Post() => CreatedAtRoute( "GetMessageById", new { id = 42 }, null ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/Values2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/Values2Controller.cs new file mode 100644 index 00000000..a58b27fb --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/Values2Controller.cs @@ -0,0 +1,13 @@ +namespace Microsoft.AspNetCore.Mvc.Basic.Controllers +{ + using Microsoft.AspNetCore.Mvc; + using System; + + [ApiVersion( "2.0" )] + [Route( "api/values" )] + public class Values2Controller : Controller + { + [HttpGet] + public IActionResult Get() => Ok( new { Controller = nameof( Values2Controller ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/ValuesController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/ValuesController.cs new file mode 100644 index 00000000..822c8350 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/Controllers/ValuesController.cs @@ -0,0 +1,13 @@ +namespace Microsoft.AspNetCore.Mvc.Basic.Controllers +{ + using Microsoft.AspNetCore.Mvc; + using System; + + [ApiVersion( "1.0" )] + [Route( "api/[controller]" )] + public class ValuesController : Controller + { + [HttpGet] + public IActionResult Get() => Ok( new { Controller = nameof( ValuesController ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_query_string_versioned_Controller_split_into_two_types.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_query_string_versioned_Controller_split_into_two_types.cs new file mode 100644 index 00000000..2b7db285 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_query_string_versioned_Controller_split_into_two_types.cs @@ -0,0 +1,53 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Basic; + using Microsoft.AspNetCore.Mvc.Basic.Controllers; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_Controller_split_into_two_types : BasicAcceptanceTest + { + [Theory] + [InlineData( nameof( ValuesController ), "1.0" )] + [InlineData( nameof( Values2Controller ), "2.0" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + + + // act + var response = await GetAsync( $"api/values?api-version={apiVersion}" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" ); + content.ShouldBeEquivalentTo( + new Dictionary() + { + ["controller"] = controller, + ["version"] = apiVersion + } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/values?api-version=3.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_url_versioned_Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_url_versioned_Controller.cs new file mode 100644 index 00000000..3e6a2759 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Basic/given/a_url_versioned_Controller.cs @@ -0,0 +1,71 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Basic; + using Microsoft.AspNetCore.Mvc.Basic.Controllers; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_Controller : BasicAcceptanceTest + { + [Theory] + [InlineData( "api/v1/helloworld", null )] + [InlineData( "api/v1/helloworld/42", "42" )] + public async Task _get_should_return_200( string requestUrl, string id ) + { + // arrange + var body = new Dictionary() + { + ["controller"] = nameof( HelloWorldController ), + ["version"] = "1" + }; + + if ( !string.IsNullOrEmpty( id ) ) + { + body["id"] = id; + } + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); + content.ShouldBeEquivalentTo( body ); + } + + [Fact] + public async Task _post_should_return_201() + { + // arrange + var entity = default( object ); + + // act + var response = await PostAsync( "api/v1/helloworld", entity ).EnsureSuccessStatusCode(); + + // assert + response.Headers.Location.Should().Be( new Uri( "http://localhost/api/v1/HelloWorld/42" ) ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/v2/helloworld" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/ByNamespaceAcceptanceTest.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/ByNamespaceAcceptanceTest.cs new file mode 100644 index 00000000..535820c0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/ByNamespaceAcceptanceTest.cs @@ -0,0 +1,25 @@ +namespace Microsoft.AspNetCore.Mvc.ByNamespace +{ + using AspNetCore.Routing; + using Builder; + using System.Reflection; + using Versioning; + + public abstract class ByNamespaceAcceptanceTest : AcceptanceTest + { + protected ByNamespaceAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( Controllers.V1.AgreementsController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( Controllers.V2.AgreementsController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( Controllers.V3.AgreementsController ).GetTypeInfo() ); + } + + protected override void OnConfigureRoutes( IRouteBuilder routeBuilder ) + { + routeBuilder.MapRoute( "VersionedQueryString", "api/{controller}/{accountId}/{action=Get}" ); + routeBuilder.MapRoute( "VersionedUrl", "v{version:apiVersion}/{controller}/{accountId}/{action=Get}" ); + } + + protected override void OnAddApiVersioning( ApiVersioningOptions options ) => options.ReportApiVersions = true; + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V1/AgreementsController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V1/AgreementsController.cs new file mode 100644 index 00000000..40baf178 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V1/AgreementsController.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V1 +{ + using Models; + + [ApiVersion( "1.0" )] + public class AgreementsController : Controller + { + public IActionResult Get( string accountId ) => Ok( new Agreement( GetType().FullName, accountId, HttpContext.GetRequestedApiVersion().ToString() ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V2/AgreementsController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V2/AgreementsController.cs new file mode 100644 index 00000000..0b011ec0 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V2/AgreementsController.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V2 +{ + using Models; + + [ApiVersion( "2.0" )] + public class AgreementsController : Controller + { + public IActionResult Get( string accountId ) => Ok( new Agreement( GetType().FullName, accountId, HttpContext.GetRequestedApiVersion().ToString() ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V3/AgreementsController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V3/AgreementsController.cs new file mode 100644 index 00000000..0f96c3d3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Controllers/V3/AgreementsController.cs @@ -0,0 +1,10 @@ +namespace Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V3 +{ + using Models; + + [ApiVersion( "3.0" )] + public class AgreementsController : Controller + { + public IActionResult Get( string accountId ) => Ok( new Agreement( GetType().FullName, accountId, HttpContext.GetRequestedApiVersion().ToString() ) ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Models/Agreement.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Models/Agreement.cs new file mode 100644 index 00000000..62504f77 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/Models/Agreement.cs @@ -0,0 +1,20 @@ +namespace Microsoft.AspNetCore.Mvc.ByNamespace.Models +{ + using System; + + public class Agreement + { + public Agreement( string controller, string accountId, string apiVersion ) + { + Controller = controller; + AccountId = accountId; + ApiVersion = apiVersion; + } + + public string Controller { get; set; } + + public string AccountId { get; set; } + + public string ApiVersion { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_query_string_versioned_Controller_per_namespace.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_query_string_versioned_Controller_per_namespace.cs new file mode 100644 index 00000000..2819c6e8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_query_string_versioned_Controller_per_namespace.cs @@ -0,0 +1,47 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.ByNamespace; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_Controller_per_namespace : ByNamespaceAcceptanceTest + { + [Theory] + [InlineData( "Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V1.AgreementsController", "1.0" )] + [InlineData( "Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V2.AgreementsController", "2.0" )] + [InlineData( "Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V3.AgreementsController", "3.0" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + var example = new { controller = "", apiVersion = "", accountId = "" }; + + // act + var response = await GetAsync( $"api/agreements/42?api-version={apiVersion}" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsExampleAsync( example ); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( new { controller = controller, apiVersion = apiVersion, accountId = "42" } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/agreements/42?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_url_versioned_Controller_per_namespace.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_url_versioned_Controller_per_namespace.cs new file mode 100644 index 00000000..090406c9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/ByNamespace/given/a_url_versioned_Controller_per_namespace.cs @@ -0,0 +1,47 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.ByNamespace; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_Controller_per_namespace : ByNamespaceAcceptanceTest + { + [Theory] + [InlineData( "Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V1.AgreementsController", "1" )] + [InlineData( "Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V2.AgreementsController", "2" )] + [InlineData( "Microsoft.AspNetCore.Mvc.ByNamespace.Controllers.V3.AgreementsController", "3" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + var example = new { controller = "", apiVersion = "", accountId = "" }; + + // act + var response = await GetAsync( $"v{apiVersion}/agreements/42" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsExampleAsync( example ); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( new { controller = controller, apiVersion = apiVersion, accountId = "42" } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "v4/agreements/42" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/HelloWorldController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/HelloWorldController.cs new file mode 100644 index 00000000..4f487ef4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/HelloWorldController.cs @@ -0,0 +1,16 @@ +namespace Microsoft.AspNetCore.Mvc.Conventions.Controllers +{ + using AspNetCore.Routing; + using Microsoft.AspNetCore.Mvc; + using System; + + [Route( "api/v{version:apiVersion}/[controller]" )] + public class HelloWorldController : Controller + { + [HttpGet] + public IActionResult Get() => Ok( new { Controller = nameof( HelloWorldController ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpGet( "{id:int}" )] + public IActionResult Get( int id ) => Ok( new { Controller = nameof( HelloWorldController ), Id = id, Version = HttpContext.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/Values2Controller.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/Values2Controller.cs new file mode 100644 index 00000000..14b52ed3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/Values2Controller.cs @@ -0,0 +1,21 @@ +namespace Microsoft.AspNetCore.Mvc.Conventions.Controllers +{ + using Microsoft.AspNetCore.Mvc; + using System; + + [Route( "api/values" )] + public class Values2Controller : Controller + { + [HttpGet] + public IActionResult Get() => Ok( new { Controller = nameof( Values2Controller ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpGet( "{id:int}" )] + public IActionResult Get( int id ) => Ok( new { Controller = nameof( Values2Controller ), Id = id, Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpGet] + public IActionResult GetV3() => Ok( new { Controller = nameof( Values2Controller ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpGet( "{id:int}" )] + public IActionResult GetV3( int id ) => Ok( new { Controller = nameof( Values2Controller ), Id = id, Version = HttpContext.GetRequestedApiVersion().ToString() } ); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/ValuesController.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/ValuesController.cs new file mode 100644 index 00000000..30b11769 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/Controllers/ValuesController.cs @@ -0,0 +1,15 @@ +namespace Microsoft.AspNetCore.Mvc.Conventions.Controllers +{ + using Microsoft.AspNetCore.Mvc; + using System; + + [Route( "api/[controller]" )] + public class ValuesController : Controller + { + [HttpGet] + public IActionResult Get() => Ok( new { Controller = nameof( ValuesController ), Version = HttpContext.GetRequestedApiVersion().ToString() } ); + + [HttpGet( "{id:int}" )] + public IActionResult Get( int id ) => Ok( new { Controller = nameof( ValuesController ), Id = id, Version = HttpContext.GetRequestedApiVersion().ToString() } ); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/ConventionsAcceptanceTest.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/ConventionsAcceptanceTest.cs new file mode 100644 index 00000000..585d4df4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/ConventionsAcceptanceTest.cs @@ -0,0 +1,32 @@ +namespace Microsoft.AspNetCore.Mvc.Conventions +{ + using Controllers; + using System.Reflection; + using Versioning; + using Versioning.Conventions; + + public abstract class ConventionsAcceptanceTest : AcceptanceTest + { + protected ConventionsAcceptanceTest() + { + FilteredControllerTypes.Add( typeof( ValuesController ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( Values2Controller ).GetTypeInfo() ); + FilteredControllerTypes.Add( typeof( HelloWorldController ).GetTypeInfo() ); + } + + protected override void OnAddApiVersioning( ApiVersioningOptions options ) + { + options.ReportApiVersions = true; + options.Conventions.Controller().HasApiVersion( 1, 0 ); + options.Conventions.Controller() + .HasApiVersion( 2, 0 ) + .HasApiVersion( 3, 0 ) + .Action( c => c.GetV3() ).MapToApiVersion( 3, 0 ) + .Action( c => c.GetV3( default( int ) ) ).MapToApiVersion( 3, 0 ); + options.Conventions.Controller() + .HasApiVersion( 1, 0 ) + .HasApiVersion( 2, 0 ) + .AdvertisesApiVersion( 3, 0 ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_query_string_versioned_Controller_split_into_two_types_using_conventions.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_query_string_versioned_Controller_split_into_two_types_using_conventions.cs new file mode 100644 index 00000000..aefac687 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_query_string_versioned_Controller_split_into_two_types_using_conventions.cs @@ -0,0 +1,48 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Conventions; + using Microsoft.AspNetCore.Mvc.Conventions.Controllers; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_query_string_versioned_Controller_split_into_two_types_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( nameof( ValuesController ), "1.0" )] + [InlineData( nameof( Values2Controller ), "2.0" )] + [InlineData( nameof( Values2Controller ), "3.0" )] + public async Task _get_should_return_200( string controller, string apiVersion ) + { + // arrange + var example = new { controller = "", version = "" }; + + // act + var response = await GetAsync( $"api/values?api-version={apiVersion}" ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsExampleAsync( example ); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( new { controller = controller, version = apiVersion } ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/values?api-version=4.0" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_url_versioned_Controller_using_conventions.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_url_versioned_Controller_using_conventions.cs new file mode 100644 index 00000000..06cb2ee8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Conventions/given/a_url_versioned_Controller_using_conventions.cs @@ -0,0 +1,59 @@ +namespace given +{ + using FluentAssertions; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Conventions; + using Microsoft.AspNetCore.Mvc.Conventions.Controllers; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + using Xunit; + using static System.Net.HttpStatusCode; + + public class _a_url_versioned_Controller_using_conventions : ConventionsAcceptanceTest + { + [Theory] + [InlineData( "api/v1/helloworld", "1", null )] + [InlineData( "api/v2/helloworld", "2", null )] + [InlineData( "api/v1/helloworld/42", "1", "42" )] + [InlineData( "api/v2/helloworld/42", "2", "42" )] + public async Task _get_should_return_200( string requestUrl, string apiVersion, string id ) + { + // arrange + var body = new Dictionary() + { + ["controller"] = nameof( HelloWorldController ), + ["version"] = apiVersion + }; + + if ( !string.IsNullOrEmpty( id ) ) + { + body["id"] = id; + } + + // act + var response = await GetAsync( requestUrl ).EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsAsync>(); + + // assert + response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); + content.ShouldBeEquivalentTo( body ); + } + + [Fact] + public async Task _get_should_return_400_when_version_is_unsupported() + { + // arrange + + + // act + var response = await GetAsync( "api/v3/helloworld" ); + var content = await response.Content.ReadAsAsync(); + + // assert + response.StatusCode.Should().Be( BadRequest ); + content.Error.Code.Should().Be( "UnsupportedApiVersion" ); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/FilteredControllerFeatureProvider.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/FilteredControllerFeatureProvider.cs new file mode 100644 index 00000000..0b425bcb --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/FilteredControllerFeatureProvider.cs @@ -0,0 +1,32 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using Controllers; + using System.Collections; + using System.Collections.Generic; + using System.Reflection; + + internal sealed class FilteredControllerFeatureProvider : ControllerFeatureProvider, ICollection + { + private readonly HashSet controllerTypes = new HashSet(); + + protected override bool IsController( TypeInfo typeInfo ) => base.IsController( typeInfo ) && controllerTypes.Contains( typeInfo ); + + public int Count => controllerTypes.Count; + + public bool IsReadOnly => false; + + public void Add( TypeInfo item ) => controllerTypes.Add( item ); + + public void Clear() => controllerTypes.Clear(); + + public bool Contains( TypeInfo item ) => controllerTypes.Contains( item ); + + public void CopyTo( TypeInfo[] array, int arrayIndex ) => controllerTypes.CopyTo( array, arrayIndex ); + + public IEnumerator GetEnumerator() => controllerTypes.GetEnumerator(); + + public bool Remove( TypeInfo item ) => controllerTypes.Remove( item ); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpContentExtensions.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpContentExtensions.cs new file mode 100644 index 00000000..a7d916a1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/HttpContentExtensions.cs @@ -0,0 +1,11 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + + internal static class HttpContentExtensions + { + internal static Task ReadAsExampleAsync( this HttpContent content, T example ) => content.ReadAsAsync(); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Microsoft.AspNetCore.Mvc.Acceptance.Tests.xproj b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Microsoft.AspNetCore.Mvc.Acceptance.Tests.xproj new file mode 100644 index 00000000..54f6a178 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Microsoft.AspNetCore.Mvc.Acceptance.Tests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 4eed304c-d1a6-4866-8d7f-450d084fd25d + Microsoft.AspNetCore.Mvc + .\obj + .\bin\ + v4.5 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiError.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiError.cs new file mode 100644 index 00000000..a0ae0e34 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiError.cs @@ -0,0 +1,13 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using System; + + public class OneApiError + { + public string Code { get; set; } + + public string Message { get; set; } + + public OneApiInnerError InnerError { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiErrorResponse.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiErrorResponse.cs new file mode 100644 index 00000000..b16e5583 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiErrorResponse.cs @@ -0,0 +1,9 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using System; + + public class OneApiErrorResponse + { + public OneApiError Error { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiInnerError.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiInnerError.cs new file mode 100644 index 00000000..2a9aa00a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/OneApiInnerError.cs @@ -0,0 +1,11 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using System; + + public class OneApiInnerError + { + public string Code { get; set; } + + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..abf073a8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.AspNetCore.Mvc.Acceptance.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4eed304c-d1a6-4866-8d7f-450d084fd25d")] diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/TaskExtensions.cs b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/TaskExtensions.cs new file mode 100644 index 00000000..3d5f6ce8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/TaskExtensions.cs @@ -0,0 +1,16 @@ +namespace Microsoft.AspNetCore.Mvc +{ + using System; + using System.Net.Http; + using System.Threading.Tasks; + + internal static class TaskExtensions + { + internal static async Task EnsureSuccessStatusCode( this Task task ) + { + var response = await task; + response.EnsureSuccessStatusCode(); + return response; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/project.json b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/project.json new file mode 100644 index 00000000..449c67ac --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/project.json @@ -0,0 +1,37 @@ +{ + "version": "1.0.0-*", + "testRunner": "xunit", + + "dependencies": { + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Microsoft.AspNetCore.TestHost": "1.0.0", + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0" + }, + "FluentAssertions": "4.12.0", + "Moq": "4.6.36-alpha", + "Newtonsoft.Json": "9.0.1", + "System.Diagnostics.TraceSource": "4.0.0", + "System.Net.Http": "4.1.0", + "Microsoft.AspNetCore.Mvc.Versioning": { + "target": "project", + "version": "" + }, + "more.xunit": "2.2.0-beta2-build3300", + "Microsoft.AspNet.WebApi.Client": "5.2.3", + "System.Runtime.Serialization.Xml": "4.1.1" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ "dnxcore50", "portable-net451+win8" ] + } + }, + + "buildOptions": { + "copyToOutput": { + "include": [ "xunit.runner.json" ] + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/xunit.runner.json b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/xunit.runner.json new file mode 100644 index 00000000..bb4656dc --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Acceptance.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "methodDisplay": "31" +}