2017-10-11

Unittesting custom Sitecore SXA components using FakeDb part 2: Mocking and testing

In part 1, I discussed services necessary to get your repository unittests to work if you use ModelRepository.FillBaseProperties() in your code. In this part, I'll show how to mock these services and round up to demonstrate a working test.

Mocking Sitecore services

Using NSubstitute, we can easily create mocks for the necessary service interfaces to use in the mock ServiceCollection. IRendering and IPageContext :

private static IRendering MockRendering(Db fakeDb)
{
 var renderingMock = Substitute.For<IRendering>();
 renderingMock.DataSourceItem.Returns(fakeDb.GetItem(ItemId));
 return renderingMock;
}

private static IPageContext MockPageContext(Db fakeDb)
{
 var contextMock = Substitute.For<IPageContext>();
 contextMock.Current.Returns(fakeDb.GetItem(ItemId));
 return contextMock;
}

private static IControlEditabilityService MockIControlEditabilityService()
{
 var mock = Substitute.For<IControlEditabilityService>();
 var editabilityMock = Substitute.For<IControlEditability>();
 editabilityMock.Editable.Returns(false);
 mock.GetControlEditability(Arg.Any<IRendering>()).Returns(editabilityMock);
 return mock;
}

Note that both IRendering and IPageContext get the item you're going to mock per unittest and that's already in the FakeDb. The items they return should be the same and be easily specifyable in each TestMethod, so I've added this line to my test baseclass:

protected static ID ItemId = ID.NewID;

This ID is then used to create, add and retrieve a DbItem to and from the FakeDb. This same item also needs to be set as the current item in the Context:

Context.Items["sc_CurrentItem"] = fakeDb.GetItem(ItemId);

Usage in testcases

Because all the mocking work needed to run the tests is done in a baseclass, all you need in unit-specific test class is a way to get your repository (I've made the baseclass using generics so it automatically registers IMyService with MyService as implementation class in the ServiceCollection):

private MyRepository Repository
{
 get
 {
  return ServiceLocator.Current.ServiceProvider.GetService(typeof(IMyRepository)) as MyRepository;
 }
}

and, of course, tying it all up in a testcase, where SetupServices is described in the previous post:

[TestMethod]
public void FillExtraProperties_FillsTitle()
{
 using (var fakeDb = new Db())
 {
  // Arrange
  var item = new DbItem("MyItemName", ItemId, SitecoreItemConstants.TemplateId) {
   { SitecoreItemConstants.TitleFieldSelector, "testtitel" }
  };
  fakeDb.Add(item);
  SetupServices(fakeDb).FinalizeSetup();

  // Act
  var result = Repository.GetModel() as MykModel;

  // Assert
  Assert.IsNotNull(result);
  Assert.AreEqual("testtitel", result.MyTitle);
 }
}

And there you have it! If there's anything still unclear with the process don't hesitate to drop a comment. Happy testing!

2017-09-28

Unittesting custom Sitecore SXA components using FakeDb part 1: Setting up

When you create custom components for Sitecore, you're probably doing so because you've got a piece of logic for rendering certain information that simply isn't provided out-of-the-box. But proving (read: unit testing) that logic can be a bit tricky when everything in your component hooks into the Sitecore API.

Frameworks

For this article, I'm using NSubstitute but this method would work just as well with your mocking framework of choice.

The most important part however is FakeDb. FakeDb allows you to mock items in the Sitecore master database to test your repository and controller.

Mocking the Sitecore context

When you make a custom Sitecore component, there's a lot of "magic" going on in the background.  You can get the current URL, the data item that was configured for the component, even query everything else on the site if you really want to, for some reason.

This magic however also makes for tricky unittesting, because you aren't actually IN Sitecore when running your tests (you can be, by the way, but that would make your tests more like integration tests and that's a whole other part of the testing pyramid).

This post is based off examples shown in a StackOverflow post, but those examples are using the Sitecore.DependencyInjection namespace. When you make SXA components however, you're going to need the Sitecore.XA.Foundation.IoC namespace which behaves similar but slightly, infuriatingly, different.

In the steps below we'll set up unit testing for a model repository.

1. Set up your references

Most of the assembly references you need can be pulled off the official Sitecore NPM feed, but you'll need three extra;
  • Sitecore.XA.Foundation.Common
  • Sitecore.XA.Foundation.Editing
  • Sitecore.XA.Foundation.IoC
These can be found in your Sitecore Website\bin folder.
Other assemblies you'll need that can be obtained from NPM are
  • Sitecore.FakeDb
  • Sitecore.Kernel
  • Sitecore.Analytics
  • Sitecore.Analytics.Core
  • Sitecore.Analytics.Model
  • Sitecore.Logging
  • Sitecore.Mvc
  • Sitecore.Nexus
  • NSubstitute (or your choice of mocking framework, but I'll be using this in my examples)

2. Set up dependency injection

I suggest creating a base test class that your unit test classes derive from so you can make most of this plumbing just once. To be able to call FillBaseProperties from the ModelRepository base class your repository likely extends, this is the minimum amount of services you need to set up:

protected virtual void SetupServices(Db fakeDb)
{
    var mockServiceCollection = new ServiceCollection();
    new Sitecore.DependencyInjection.DefaultSitecoreServicesConfigurator().Configure(mockServiceCollection);

    mockServiceCollection.AddTransient(provider => MockRendering(fakeDb));
    mockServiceCollection.AddTransient(provider => MockPageContext(fakeDb));
    mockServiceCollection.AddTransient(provider => MockIControlEditabilityService());
    mockServiceCollection.AddTransient<IMyRepository, MyRepository>();

    var serviceProvider = mockServiceCollection.BuildServiceProvider();
    ServiceLocator.SetLocatorProvider(new Locator(serviceProvider));
    
    var instance = typeof(FieldTypeManager).GetField("Instance", BindingFlags.Static | BindingFlags.NonPublic);
    var instanceValue = instance.GetValue(null) as Sitecore.DependencyInjection.LazyResetable<Sitecore.Abstractions.BaseFieldTypeManager>;
    instanceValue.Value.Initialize();
}

3. Mocking

In the next post I'll go into mocking your services and items.

2017-08-30

Configure SOLR search with Sitecore Experience Accelerator

It's been quite a while since my last update, but today I finally was able to get something to work that gave me so much headache I thought it might help someone else if I'd put my solution in a blog.

The problem was configuring Solr search for our new Sitecore site. Normally, this is supported pretty much out of the box. However this new site also uses the awesome new Experience Accelerator, which throws a wrench into the process. It also doesn't help I wasn't able to find a single blog or Sitecore documentation site that gave a proper step-by-step guide for setting up Solr so I'll also do that here.

1. Setting up Solr itself


Grabbing the Solr package

First, we need to get Solr running. For this, I used a BitNami package. Sadly, Sitecore recommends an older version of Solr (5.1) so you'll have to dig for the right package version since BitNami doesn't maintian an archive page for its packages. Luckily, I've done that for you.

Don't install it just yet though, we'll automate all this in the next step!

SolrCannon

Kam Figy (Kamsar on Github) has created an awesome Powershell script to install Solr , configure it for your Sitecore installation and even generate a config patch file to get Sitecore to use the correct Solr cores. Grab it!

Have a close look at the # Variables section and change everything to your liking.

Schema.xml

Lastly, Solr needs a schema.xml to operate on. This is the only step that will require some manual labor because Sitecore doesn't export a correct schema.xml for indexing your SXA content.

  • First, export one from Sitecore (/sitecore/client/Applications/ControlPanel.aspx -> Generate the Solr Schema.xml file)
  • Next, open the schema.xml and add the following line near the bottom. This will make sure your SXA content is also indexed:
<dynamicField name="*_tm"
    type="text_general"
    indexed="true"
    stored="true"
    multiValued="true"/>

Fire all cannons!

Now put everything from the last steps together in one folder. 

Run SolrCannon and answer "y" to the prompt. This will automatically install the BitNami package and fully configure it with everything you need.

2. Configure Sitecore

Next, Sitecore needs to know we're using Solr.

SolrSwap

Another great script, created by Patrick Perrone, automatically switches existing Sitecore config patch files to Solr.

Run it and enter the root folder of your Sitecore instance. Next, answer "S" to have it switch Sitecore over from the standard Lucene to Solr.

You may have to manually enable [instance root]\Website\App_Config\Include\z.Foundation.Overrides\Sitecore.XA.Foundation.Search.Solr.config and disable the Lucene config.

Solr.config

After running SolrCannon, it generated a file called Solr.config. Put this in [instance root]\Website\App_Config\Include\z.yourfolder. The name of the folder can be to your liking, but it MUST start with z and be alphabetically after z.Foundation.Overrides. This is to ensure this config patch is loaded at the very last by Sitecore.

If you did this step incorrectly you'll recognize it by Sitecore throwing the following error:

Could not create instance of type: Sitecore.ContentSearch.SolrProvider.SwitchOnRebuildSolrSearchIndex. No matching constructor was found.


3. Rebuild your indexes and check for errors

If you did everything correctly, when you rebuild your indexes (/sitecore/client/Applications/ControlPanel.aspx -> Indexing manager -> Rebuild) you should see no errors in the Solr logs and all indexes are rebuilt successfully. 

If Solr logs errors, you can add the missing fields to the schema.xml. Running SolrCannon won't update this in the Solr config folder, so find the schema.xml in [Solr install root]\apache-solr\solr\configsets\[sitename]_configs\conf and replace it with the new schema.xml.

Don't forget to restart the solrApache and solrJetty services or the new schema.xml won't be loaded. Rebuild your indexes.

If all went well your indexing manager window should look like this, with document counts > 0:

Indexing manager success

4. ...?

5. Profit!

Now add a search bar and search results page to your site and you're done!

2015-01-26

Creating a .Net WebApi service that uses SAML2 authentication

In the last post, I demonstrated how to consume a cross-domain WebApi service that uses SAML2 for authentication and authorization. This is useful when you want to use a shared service layer that provides data for multiple websites. However, some code work is needed to enable this on the server side as well.

In this example we'll create a new .Net WebApi layer in Visual Studio 2013. Everything in this post can be transferred to an MVC app just as easily if needed, however Ajax calls to MVC apps are most likely on the same domain so will not need this extra work. The only difference lies in whether you register CorsConfiguration in the WebApiConfig.cs or RouteConfig.cs. That said, let's get started.

First, create a new ASP.Net Web Application. We'll call the new project POC_SAML_WebApi. When you create the project, you'll be prompted for the type of web application you wish to create. Choose WebApi:

1. Choose WebApi; 2. Change Authentication; 3. Choose No Authentication

Next, you'll also need to make sure Visual Studio won't add all kinds of unnecessary authentication code and configuration to your project, as we'll add all the necessary configuration and code ourselves. Click the Change Authentication button and choose No Authentication in the pop-up. Click OK in both screens and your project will be created.

Out of the box, .Net WebApi does not support SAML2. To facilitate this, add the Thinktecture.IdentityModel library through the NuGet Package Manager.



Next, add the following code to your WebApiConfig.cs, or merge it with the existing file. Be sure to include the relevant namespaces that Visual Studio suggests:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.AllowAll().AllowAll();
        var corsHandler = new CorsMessageHandler(corsConfig, config);
        config.MessageHandlers.Add(corsHandler);
        
        // default API route
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

This registers the Thinktecture CORS module for handling incoming WebApi calls. The corsConfig.AllowAll().AllowAll() call may need to be adjusted to your personal needs, but for this demonstration we'll use this. If you want more information about the different configuration options, have a look at this post by Brock Allen. If you want to do SAML authentication for an MVC app, simply adjust above code for your RouteConfig.cs.

The last bit of code needed for SAML2 support is in the Global.asax file, which makes sure unauthorized Ajax requests aren't promptly redirected to ADFS but simply answered with a 401 error, enabling the error handling demonstrated in the last post:

protected void Application_BeginRequest(object s, EventArgs ea)
{
    FederatedAuthentication.WSFederationAuthenticationModule.AuthorizationFailed += (sender, e) =>
    {
        if (new HttpRequestWrapper(System.Web.HttpContext.Current.Request).IsAjaxRequest())
        {
            e.RedirectToIdentityProvider = false;
        }
    };
}
Of course, you should make sure your JavaScript calling this WebApi actually adds the X-Requested-With header for this to work.

Last but certainly not least are changes required in the Web.config file. These settings tell your application where your authentication server resides and how your application should communicate with it.


  <configSections>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  </configSections>
 
  <appSettings>
    ...
    <add key="ida:AdfsMetadataEndpoint" value="[AD FS Federation Metadata endpoint]" />
    <add key="ida:Audience" value="[WebApi address]" />
  </appSettings>
 
  <system.webServer>   
    <modules>
      <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
      <add name="WSFederationAuthenticationModule" type="System.IdentityModel.Services.WSFederationAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler" />
    </modules>
  </system.webServer>
 
  <system.identityModel>
    <identityConfiguration>
      <audienceUris>
        <add value="[WebApi address]" />
      </audienceUris>
      <issuerNameRegistry>
        <trustedIssuers>
          <add name="[AD FS certificate name]" 
               thumbprint="[AD FS certificate thumbprint]" />
        </trustedIssuers>
      </issuerNameRegistry>
    </identityConfiguration>
  </system.identityModel>
  <system.identityModel.services>
    <federationConfiguration>
      <wsFederation passiveRedirectEnabled="true" 
                    issuer="[AD FS SAML2 endpoint address]" 
                    realm="[WebApi realm]" requireHttps="true" />
      <cookieHandler requireSsl="true" />
    </federationConfiguration>
  </system.identityModel.services>

In these configuration settings are a couple of things you need to adjust yourself (just find/replace the following strings):
  • [AD FS Federation Metadata endpoint]: This is the endpoint address for ADFS ending in "/FederationMetadata/2007-06/FederationMetadata.xml", for instance "https://adfs.mycompany.com/FederationMetadata/2007-06/FederationMetadata.xml";
  • [WebApi address]: The root address where your new WebApi will be running, for instance "https://poc-saml-webapi.mycompany.com:443";
  • [AD FS certificate name]: The name of the certificate you configured in ADFS for token signing;
  • [AD FS certificate thumbprint]: The thumbprint of the certificate mentioned above;
  • [AD FS SAML2 endpoint address]: The endpoint address for ADFS ending in "/adfs/ls"
  • [WebApi realm]: The realm you'll configure in ADFS by which your WebApi server can be identified, for instance "urn:services.mycompany.com:webapi". You can pick any valid URI for this setting but it's common practice to use a urn: name here.
With this, your service is ready to be published in IIS. Make sure to configure an HTTPS binding and use the address that you configured in [WebApi address] (or vice versa, of course). One last important thing you need to do, which is often overlooked, is to enable anonymous authentication in IIS. If you don't, the OPTIONS call that is part of the CORS pre-flight will not be accepted:

For your website running the WebApi service, select Authentication under the IIS header

Next, select Anonymous Authentication and click Enable

Stay tuned for a demonstration on how to configure the settings needed in ADFS itself to enable single sign-on for this WebApi service or another relying party.

2015-01-21

Calling a cross-domain web service using JavaScript and SAML2


When you have a client application and a webservice application but they both reside on different (sub-) domains, you'll want to link them using single sign-on, using for instance ADFS, or your user experience will suffer. Nowadays with OAuth2, this isn't too much of an issue. But when you're forced to work with SAML2 (for instance because your infrastructure is still at Windows Server 2008R2 and ADFS v2), things aren't that simple.

Since cookies may not be shared between domains as a security measure, one must find a way around this. Luckily, ADFS supports you in this. You just need to get JavaScript to do the same.

Starting with the actual call to a WebApi service. Assume the following JavaScript is gained from a secure part of a client application, residing on https://www.mycompany.com/:

var serviceUrl = "https://services.mycompany.com/api/Employee";

function makeCall() {
    return $.ajax({
        url: serviceUrl,
        type: 'GET',
        contenttype: 'application/json',
        dataType: 'json',
        xhrFields: { withCredentials: true },
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        },
        crossDomain: true
    });
};

Using these parameters, authentication when you're already logged in is automatically handled. And since the above snippet is retrieved from a secure site, that should be the case, right? Sadly, no. We're logged into the domain of the client application, but for this example we're calling a WebApi service on another sub-domain. Because of this, invoking makeCall() will result in a 401 (not authorized) error. So, the following line is added to the JavaScript code:

$.support.cors = true;

This tells JQuery we will be making cross-domain requests and to set up CORS pre-flight checks. Sadly, JQuery (or Angular, for that matter) doesn't handle the redirects that are inherent to ADFS authentication very well, nor does it handle the Set-Cookie headers that give you the FedAuth cookies you're after. The browser, however, is stellar at this. So we let the browser handle logging in, using an iframe:

var serviceUrl = "https://services.mycompany.com/api/Employee",
    loginUrl = "https://services.mycompany.com/api/Login",
    authenticating = false,
    retryCount = 5,
    iFrame;

function authenticate() {

    return $.Deferred(function (d) {
        // Potentially could make this into a little popup layer
        // that shows we are authenticating, 
        // and allows for re-authentication if needed

        if (!authenticating) {

            authenticating = true;

            iFrame = $("<iframe></iframe>");
            iFrame.hide();
            iFrame.appendTo("body");
            iFrame.load(function (data) {
                d.resolve();
            });
           
            iFrame.attr('src', loginUrl);

         } else {
            d.resolve();
        }
    });
};

function makeCall() {
    return $.ajax({
        url: serviceUrl,
        type: 'GET',
        contenttype: 'application/json',
        dataType: 'json',
        xhrFields: { withCredentials: true },
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        },
        crossDomain: true
    })
    .error(function (error) {

        // In IE10 we can get status = 0 in case of some errors, 
        // including 401 because of a bug...
        if ((error.status == 401 || error.status == 0)
            && retryCount > 0) {
            //Authenticating,

            retryCount--;

            return authenticate().then(function () {
                // Making the call again, just wait a bit 
                // for the login to complete. Better than to keep firing 
                // requests while the iframe is still busy

                setTimeout(function () { makeCall() }, 1000);
            });
        } else {
            return $.Deferred(function (d) {
                d.reject(error);
            });
        }
    })

    .success(function (data) {

        if (iFrame) {
            iFrame.remove();
            authenticating = false;
        }

        console.log(data);
    });
};

In this case, the service behind the LoginUrl simply returns an empty result, but does enforce authorization. This makes sure the authentication and authorization process is kicked off, but won't unnecessarily burden your server. If your ADFS server and webservice are configured correctly, the user won't be prompted for credentials again and will be logged in to the webservice automatically.

So blogging is a thing...

Over the years in my career as a software engineer, I've seen (and, even if I say so myself, made), some pretty cool bits of programming. Until now, I shared this knowledge with direct colleagues and they've taught me a lot in return. I decided to start this blog as a method of streamlining that process and tap into an even greater knowledge pool of like-minded people.

Recently, I've been diving into some pretty complicated technology and in the process of sharing my newly found knowledge, I noticed people were very interested, but quickly overwhelmed by what I had to tell them. Pointing them to my research notes only made them come back with even more questions. So, this blog will be about questions I get asked about security, AD FS, certificates, systems integration and of course practical applications and issues with technologies discussed, conveying what I've learned in (hopefully) easily digestible chunks. 

I don't claim to be all-knowing, but rather I hope to ignite a conversation between those that are interested in the technologies I will be blogging about and those that are already knowledgeable in them.

Lastly, this blog makes certain assumptions about prior knowledge. I use terms like cookies, web services, authentication and authorization, cross-domain security and the like without necessarily explaining them. I'll try to keep things as clear as can be, but this is definitely a blog aimed at software developers.

So here goes! Stay posted for the first entry about Cross-domain security, starting a bit backwards with a client JavaScript implementation. Following posts will be about other links in the chain, including creating a SAML-ready WebApi service, ADFS configuration, a custom attribute store and more.