Create a fully working implementation of the by using within your application.
Rationale
Having a fully working implementation allows you to easily see what needs to be achieved, and also provides an easy way to ensure that the test suite and libraries are behaving as expected within your environment.
Step 1 - Copy files
Copy the files within the Feeds, Helpers, IdComponents, Settings, and Stores directories from the into your application, and add the dependencies and . FakeDatabase.NET is an in-memory fake database that is not persisted, and will allow you to create a quick working Open Booking API.
Inspect the Controllers and copy the files (or the contents of the files) as appropriate for your application.
Note you will be creating the following endpoints (as per the ):
Dataset Site
Open Data RPDE feeds
OrderQuote Creation (C1)
OrderQuote Creation (C2)
OrderQuote Deletion
Order Creation (B)
Order Deletion
Order Cancellation
Orders RPDE Feed
Order Status
Delete Test Dataset
Create Test Opportunity
Execute Action
Dynamic Client Update
Step 2 - Copy configuration
Inspect the Startup.cs (.NET Core) or ServiceConfig.cs (.NET Framework) and copy the services.AddSingleton<IBookingEngine>(...) configuration into your own Startup.cs or ServiceConfig.cs.
The initial objective is to get a working version of the Open Booking API using entirely fake data. So when copying these files do not modify the configuration values at this stage - simply use them as-is.
Step 2 - Controller endpoint bindings
The ResponseContent class provides a .NET version agnostic representation of HTTP responses from the Booking Engine. Two example helper methods are provided to be used with your version of .NET. See ResponseContentHelper.csin the example projects.
.NET Core
The following extension method can be used to return an Mvc.ContentResult.
To speed development, the following allows test headers to be used for accessing the API, and can be set up to allow development to proceed until Day 8 of this tutorial.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// DO NOT USE THIS IN PRODUCTION.
// This passes test API headers straight through to claims, and provides no security whatsoever.
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = TestHeaderAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = TestHeaderAuthenticationOptions.DefaultScheme;
})
.AddTestHeaderAuthenticationSupport(options => { });
services.AddAuthorization(options =>
{
// No authorization checks are performed, this just ensures that the required claims are supplied
options.AddPolicy(OpenActiveScopes.OpenBooking, policy => {
policy.RequireClaim(OpenActiveCustomClaimNames.ClientId);
policy.RequireClaim(OpenActiveCustomClaimNames.SellerId);
});
options.AddPolicy(OpenActiveScopes.OrdersFeed, policy => policy.RequireClaim(OpenActiveCustomClaimNames.ClientId));
});
}
TestHeaderAuthentication.cs
/**
* DO NOT USE THIS FILE IN PRODUCTION. This approach is for testing only, and provides no security whatsoever.
*/
public class TestHeaderAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "Test Headers";
public string Scheme => DefaultScheme;
public string AuthenticationType = DefaultScheme;
}
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddTestHeaderAuthenticationSupport(this AuthenticationBuilder authenticationBuilder, Action<TestHeaderAuthenticationOptions> options)
{
return authenticationBuilder.AddScheme<TestHeaderAuthenticationOptions, TestHeaderAuthenticationHandler>(TestHeaderAuthenticationOptions.DefaultScheme, options);
}
}
public class TestHeaderAuthenticationHandler : AuthenticationHandler<TestHeaderAuthenticationOptions>
{
public TestHeaderAuthenticationHandler(
IOptionsMonitor<TestHeaderAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Get the claims from headers if they exist
Request.Headers.TryGetValue(AuthenticationTestHeaders.ClientId, out var testClientId);
Request.Headers.TryGetValue(AuthenticationTestHeaders.SellerId, out var testSellerId);
var clientId = testClientId.FirstOrDefault();
var sellerId = testSellerId.FirstOrDefault();
// This just passes the test headers through to the claims - it does not provide any security.
var claims = new List<Claim>();
if (clientId != null) claims.Add(new Claim(OpenActiveCustomClaimNames.ClientId, clientId));
if (sellerId != null) claims.Add(new Claim(OpenActiveCustomClaimNames.SellerId, sellerId));
var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
var identities = new List<ClaimsIdentity> { identity };
var principal = new ClaimsPrincipal(identities);
var ticket = new AuthenticationTicket(principal, Options.Scheme);
// No checks are made, so this always succeeds. It's just setting the claims if they exist.
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.StatusCode = 401;
await Response.WriteAsync(OpenActiveSerializer.Serialize(new InvalidAPITokenError()));
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
Response.StatusCode = 403;
await Response.WriteAsync(OpenActiveSerializer.Serialize(new UnauthenticatedError()));
}
}
public static class AuthenticationHelper
{
private static (string clientId, Uri sellerId) GetIdsFromAuth(HttpRequestMessage request, IPrincipal principal, bool requireSellerId)
{
// NOT FOR PRODUCTION USE: Please remove this block in production
IEnumerable<string> testSellerId = null;
if (request.Headers.TryGetValues(AuthenticationTestHeaders.ClientId, out IEnumerable<string> testClientId)
&& testClientId.Count() == 1
&& (!requireSellerId || (request.Headers.TryGetValues(AuthenticationTestHeaders.SellerId, out testSellerId) && testSellerId.FirstOrDefault().ParseUrlOrNull() != null))
)
{
return (testClientId.FirstOrDefault(), testSellerId?.FirstOrDefault().ParseUrlOrNull());
}
// For production use: Get Ids from JWT
var claimsPrincipal = principal as ClaimsPrincipal;
var clientId = claimsPrincipal.GetClientId();
var sellerId = claimsPrincipal.GetSellerId().ParseUrlOrNull();
if (clientId != null && (sellerId != null || !requireSellerId))
{
return (clientId, sellerId);
}
else
{
throw new OpenBookingException(new InvalidAPITokenError());
}
}
public static (string clientId, Uri sellerId) GetIdsFromAuth(HttpRequestMessage request, IPrincipal principal)
{
return GetIdsFromAuth(request, principal, true);
}
public static string GetClientIdFromAuth(HttpRequestMessage request, IPrincipal principal)
{
return GetIdsFromAuth(request, principal, false).clientId;
}
}
Step 5 - Run Application
Clicking on the ScheduledSessions feed should return JSON in the following format:
If some links appear broken, try updating BaseUrl within Startup.cs or ServiceConfig.cs with the correct port number for your local test environment.
Step 6 - Run Test Suite
Follow the instructions below to set up the OpenActive Test Suite:
In Steps 7 and 8, the header configuration can use the default values in order to work with the Booking Engine, except that the Seller @id must be replaced with a valid Seller @id from your booking system.
In Step 9, your Dataset Site is automatically created and configured by the Booking Engine, so simply update the value of datasetSiteUrl based on the port number and path used by your .NET application when running:
Additionally you will be creating three endpoints for use with the that implement the (not for use in production):
A further endpoint is required to meet the , however this can be added as part of Day 8 as it has it is not a dependency of Days 1-7:
The Booking Engine accepts input JSON as string in order to fully control deserialisation. In order to allow the web framework to capture the body of the request as a string, for the , an InputFormatter is required. See OpenBookingInputFormatter.csin the example projects.
The Booking Engine does not handle authentication, and instead accepts claims from an authentication layer in front of it (this will be covered in more detail in ). An AuthenticationHelper is provided for .NET Core and .NET Framework that extracts OAuth 2.0 claims from access tokens, and also provides simple header alternatives to facilitate development and testing.
If you run your application and navigate to the dataset site endpoint (e.g. ), you should find the following page:
Navigating to the Orders feed (e.g. ) should return the following JSON result (an empty RPDE page):
The Booking Engine includes full support for the , so running the in 'Controlled' mode is recommended.
You will need version 14 or above installed to do this - which can installed with the Visual Studio installer.
At this stage we are still using , so all tests for whichever 'implemented' features you have configured should pass. Assuming configuration values have not been changed from the copied files, issues with the tests at this stage will very likely be due to the controller or other application configuration.