1

Stuck creating Test Class for Custom Just In Time Handler (Auth.SamlJitHandler)

I am doing a PoC and have implemented a custom handler for JIT - see below

global class CustomJITRegHandler implements Auth.SamlJitHandler {

    private class JitException extends Exception{} // Exception handling

    /****************************************************************************************
    *    @description    Main JIT
    *    @params         List of SAML / SSO Params
    *    @returns        void
    *****************************************************************************************/
    private void handleJit(boolean create, User u, Id samlSsoProviderId, Id communityId, Id portalId,
            String federationIdentifier, Map<String, String> attributes, String assertion) {

        // Enter Community User creation flow
        if(communityId != null || portalId != null) {
            handlerUser(attributes, u, federationIdentifier);
        }
    }

    /****************************************************************************************
    *    @description   Facility the user creation JIT/SSO flow
    *    @params        Map of Attribute, Instance of User, String federationId
    *    @returns       void
    *****************************************************************************************/
    private void handlerUser(Map<String,String> attributes, User u, String federationIdentifier){

        // Extract additional attributes from Map
        String accountFederatedId = attributes.get('AccountFederateId');
        String lastName = attributes.get('LastName');
        String email = attributes.get('Email');

        // SOQL to find existing Contact
        List<Contact> contacts = [SELECT Id,
                FirstName,
                LastName,
                Email,
                Account.Account_Support_Alias__c
        FROM Contact
        WHERE
        Email =: email AND
        LastName =: lastName AND
        Account.Account_Support_Alias__c =: accountFederatedId LIMIT 1];

        // Existing contact is found
        if (!contacts.isEmpty()){

            // Create Community User for existing contact
            u = createNewUser(u, contacts[0], federationIdentifier);


        } else {
            //Find Account
            List<Account> accounts = [SELECT Id,
                    Name FROM Account
                    WHERE Account_Support_Alias__c =: accountFederatedId];

            //Account is found
            if (!accounts.isEmpty()){

                // Create Contact
                Contact con = createNewContact(accounts, attributes);
                // Create User
                if (con!=null) {
                    u = createNewUser(u, con, federationIdentifier);
                }

            } else{
               // @todo implement custom exception message
            }

        }
    }

    /****************************************************************************************
    *    @description    create new contact record based on attributes
    *    @params         List of Accounts, Map of Attributes
    *    @returns        Instance of Contact object
    *    @todo           Implement full Contact field mapping
    *****************************************************************************************/
    public contact createNewContact(List<Account> accts, Map<String,String> attributes){

        Contact c = new Contact(
                                    FirstName = 'SSO',
                                    LastName = attributes.get('LastName'),
                                    Email= attributes.get('Email'),
                                    AccountId = accts[0].Id);
        insert c;

        return c;
    }

    /****************************************************************************************
     *   @description    create new contact record based on attributes
     *   @params         User object, Contact object, String
     *   @returns        Instance of User object
     *   @todo           Implement full User field mapping
     ****************************************************************************************/
    public user createNewUser(user u, Contact con, String federationIdentifier) {
        // Create Community User

        u.IsActive = true;
        u.Username = con.Email + 'BT';
        u.Email = con.Email + 'BT';
        u.FirstName = con.FirstName;
        u.LastName = con.LastName;
        u.CommunityNickname = 'nickSSo'+generateRandomString(6);
        u.ContactId = con.Id;
        u.Alias = 'BT';
        u.TimeZoneSidKey = 'America/Phoenix'; // Required
        u.LocaleSidKey = 'en_US'; // Required
        u.EmailEncodingKey = 'ISO-8859-1'; // Required
        u.LanguageLocaleKey = 'en_US'; // Required
        u.ProfileId = '00e0Y000000Y5mu'; // Insert Profile Id from 7SBT org 00e0Y000001qX7Y
        u.FederationIdentifier = federationIdentifier;
        try {
            insert u;
        } catch (DmlException e){
            System.debug('**** DMLException **** ' + e);
        }
        return u;
    }

    /****************************************************************************************
    *   @description    Required method for Auth.SamlJitHandler interface
    *   @params         Id SamlSsoProviderId
    *   @returns        Instance of User
    ****************************************************************************************/
    global User createUser(Id samlSsoProviderId,
            Id communityId,
            Id portalId,
            String federationIdentifier,
            Map<String, String> attributes,
            String assertion) {

        User u = new User();
        handleJit(true, u, samlSsoProviderId, communityId, portalId,
                federationIdentifier, attributes, assertion);
        return u;

    }

    /****************************************************************************************
    *   @description    Required method for Auth.SamlJitHandler interface
    *   @params         Id SamlSsoProviderId
    *   @returns        Void
    *   @todo           Implement any existing user updates (e.g. change of active flag...)
    ****************************************************************************************/
    global void updateUser(Id userId,
            Id samlSsoProviderId,
            Id communityId,
            Id portalId,
            String federationIdentifier,
            Map<String, String> attributes,
            String assertion) {
        system.debug('-----userId---------'+userId);
        User u = [SELECT Id FROM User WHERE Id=:userId];
        // We don't need to update the User for the POC

    }

    /****************************************************************************************
    *   @description    Utility method for generating ramdom string
    *   @params         Integer length
    *   @returns        A random String of text
    ****************************************************************************************/
    public static String generateRandomString(Integer len) {
        final String chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
        String randStr = '';
        while (randStr.length() < len) {
           Integer idx = Math.mod(Math.abs(Crypto.getRandomInteger()), chars.length());
           randStr += chars.substring(idx, idx+1);
        }
        return randStr; 
    }

}

I am completely stuck on the Test Class front - here is what I have

@isTest(SeeAllData=false)
private class BTCustomJITRegHandlerTest {

    @testSetup static void testData(){

        Account a = (Account)TestFactory.createSObject(new Account());
        insert a;

        Contact c = (Contact)TestFactory.createSObject(new Contact (AccountId = a.Id));
        insert c;

        // @TODO insert test users

    }

    @isTest static void testCommunityKnown(){

        Account testAccount = [select Id, Name from Account];
        System.debug('**** testAccount ******' + testAccount);

        Contact testContact = [select Id, FirstName, LastName, AccountId, Account.Name from Contact];
        System.debug('**** testContact ******' + testContact);

        BTCustomJITRegHandler handler = new BTCustomJITRegHandler();

        Id samlSsoProviderId = '0LE0Y0000008Wkj'; //@TODO make dynamic
        Id communityId = '0DB0Y0000000WEsWAM'; //@TODO make dynamic
        Id portalId = '0DB0Y0000000WEsWAM';
        String federationIdentifier = 'test';
        Map<String, String> attributes = null;
        String assertion = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIERlc3RpbmF0aW9uPSJodHRwczovLzdzYnQtcG9jLmZvcmNlLmNvbS9obWUvbG9naW4/c289MDBEMFkwMDAwMDM0QjFoIiBJRD0iXzZiM2E1MGE0LTdhZGE4NWRiIiBJc3N1ZUluc3RhbnQ9IjIwMTctMDktMjRUMTI6NDU6MjYuNjgyWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIi8+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2RzYS1zaGExIi8+PGRzOlJlZmVyZW5jZSBVUkk9IiNfNmIzYTUwYTQtN2FkYTg1ZGIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8+PC9kczpUcmFuc2Zvcm0+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT5PKzZsSmhCbUtKZU8ra1V3bDBycmxrNTFEbzA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmlOa0JLVStOTzVUbWluQmY4WDlkdlhMc3hYODBVL1JDMjlzaVczbWJpTUgvakJFUllSQVUvQT09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEMHpDQ0E1R2dBd0lCQWdJRUYvdUZJVEFMQmdjcWhrak9PQVFEQlFBd2dib3hDekFKQmdOVkJBWVRBbFZUTVFzd0NRWURWUVFJCkV3SkRRVEVXTUJRR0ExVUVCeE1OVTJGdUlFWnlZVzVqYVhOamJ6RVNNQkFHQTFVRUNoTUpRWGhwYjIwZ1UxTlBNVkV3VHdZRFZRUUwKRTBoR1QxSWdSRVZOVDA1VFZGSkJWRWxQVGlCUVZWSlFUMU5GVXlCUFRreFpMaUJFVHlCT1QxUWdWVk5GSUVaUFVpQlFVazlFVlVOVQpTVTlPSUVWT1ZrbFNUMDVOUlU1VVV5NHhIekFkQmdOVkJBTVRGa0Y0YVc5dElFUmxiVzhnUTJWeWRHbG1hV05oZEdVd0hoY05NVFF3Ck5qSXdNRFF6TURJM1doY05OREV4TVRBMU1EUXpNREkzV2pDQnVqRUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdUQWtOQk1SWXcKRkFZRFZRUUhFdzFUWVc0Z1JuSmhibU5wYzJOdk1SSXdFQVlEVlFRS0V3bEJlR2x2YlNCVFUwOHhVVEJQQmdOVkJBc1RTRVpQVWlCRQpSVTFQVGxOVVVrRlVTVTlPSUZCVlVsQlBVMFZUSUU5T1RGa3VJRVJQSUU1UFZDQlZVMFVnUms5U0lGQlNUMFJWUTFSSlQwNGdSVTVXClNWSlBUazFGVGxSVExqRWZNQjBHQTFVRUF4TVdRWGhwYjIwZ1JHVnRieUJEWlhKMGFXWnBZMkYwWlRDQ0FiZ3dnZ0VzQmdjcWhrak8KT0FRQk1JSUJId0tCZ1FEOWYxT0JIWFVTS1ZMZlNwd3U3T1RuOWhHM1VqenZSQURESGorQXRsRW1hVVZkUUNKUisxazlqVmo2djhYMQp1akQyeTV0VmJOZUJPNEFkTkcveVptQzNhNWxRcGFTZm4rZ0VleEFpd2srN3FkZit0OFliK0R0WDU4YW9waFVQQlB1RDl0UEZIc01DCk5WUVRXaGFSTXZaMTg2NHJZZGNxNy9JaUF4bWQwVWdCeHdJVkFKZGdVSThWSXd2TXNwSzVncUxyaEF2d1dCejFBb0dCQVBmaG9JWFcKbXozZXk3eXJYRGE0VjdsNWxLKzcranJxZ3ZsWFRBczlCNEpuVVZsWGpyclVXVS9tY1FjUWdZQzBTUlp4SStoTUtCWVR0ODhKTW96SQpwdUU4Rm5xTFZIeU5LT0Nqcmg0cnM2WjFrVzZqZnd2NklUVmk4ZnRpZWdFa084eWs4YjZvVVpDSnFJUGY0VnJsbndhU2kyWmVnSHRWCkpXUUJURHYrejBrcUE0R0ZBQUtCZ1FDWHIxbXA0VXZCeVk2ZEdiRE95cTN3TXM2TzdNQ3htRWtVMngzMkFrRXA2czdYZml5M01Zd0sKd1pRNHNMNEJtUVl6WjdRT1hQUDhkS2dyS0RRS0xrOXRYV09ndklvT0NpTkFkUURZbFJtMnNZZ3JJMlNVY3lNMWJLRHFMd0REOFo1TwpvTGV1UUF0Z01mQXEvZjFDNm5SRVdyUXVkUHhPd2FvTmRIa1ljUiswNjZNaE1COHdIUVlEVlIwT0JCWUVGRTJKQWM5N3dmSEs1YjQyCm5LYkFObjRTTWNxY01Bc0dCeXFHU000NEJBTUZBQU12QURBc0FoUitDanZwOFV3TmdLSGZ4MlBXSm9SaTAvMXE4QUlVTmhUWFdsR3oKSjNTZEJsZ1JzZEZnS3lGdGN4RT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDJwOlN0YXR1cz48c2FtbDJwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbDJwOlN0YXR1cz48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iXzUyZTc4OGFhLTEwZmMyMTgyIiBJc3N1ZUluc3RhbnQ9IjIwMTctMDktMjRUMTI6NDU6MjYuNjgxWiIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyLz48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWQiPmFnZW50Z2lsbDc0QGdtYWlsLmNvbTwvc2FtbDI6TmFtZUlEPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxNy0wOS0yNFQxMjo0NjoyNi42ODF

    aIiBSZWNpcGllbnQ9Imh0dHBzOi8vN3NidC1wb2MuZm9yY2UuY29tL2htZS9sb2dpbj9zbz0wMEQwWTAwMDAwMzRCMWgiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNy0wOS0yNFQxMjo0NToyNi42ODFaIiBOb3RPbk9yQWZ0ZXI9IjIwMTctMDktMjRUMTI6NDY6MjYuNjgxWiI+PHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWwyOkF1ZGllbmNlPmh0dHBzOi8vc2FtbC5zYWxlc2ZvcmNlLmNvbTwvc2FtbDI6QXVkaWVuY2U+PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE3LTA5LTI0VDEyOjQ1OjI2LjY4MVoiPjxzYW1sMjpBdXRobkNvbnRleHQ+PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOnVuc3BlY2lmaWVkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50PjxzYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJzc29TdGFydFBhZ2UiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5odHRwOi8vYXhpb21zc28uaGVyb2t1YXBwLmNvbS9SZXF1ZXN0U2FtbFJlc3BvbnNlLmFjdGlvbjwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJsb2dvdXRVUkwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIi8+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJvcmdhbml6YXRpb25faWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj4wMEQwWTAwMDAwMzRCMWg8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgTmFtZT0icG9ydGFsX2lkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+MERCMFkwMDAwMDAwV0VzV0FNPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48L3NhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4='; //@TODO insert test SAML assertion XML


            handler.createUser(samlSsoProviderId, communityId, portalId, federationIdentifier, attributes, assertion);



        }

    }

I have no idea how I need to mock up the assertion and test. I assume there are some methods User.Data for Auth.SamUtiHandler which I need to call which before calling the methods or do something else?

Test class is giving System.NullPointerException: Attempt to de-reference a null object

Any advice / steering in right direction much welcome.

1 Answer 1

3

First off, the test is failing because your attributes Map is null.

The trick with testing these sorts of things is the only test the code you've written, don't try testing things that are not there or standard Salesforce functionality (if you don't have to).

Your createUser method doesn't even make use of the samlSsoProviderId, communityId and portalId variables, so don't bother making them real Id, dummy Ids will do, e.g. 0LE000000000000' forsamlSsoProviderId`.

You also don't need to worry about User.Data or Auth.SamlJitHandler because you don't need to test what they do.

Here is a basic outline of what the test class could look like with a single test for your createUser method (obviously it needs more tests methods written).

@isTest
private class BTCustomJITRegHandlerTest {
    private final String ACCOUNT_SUPPORT_ALIAS = 'accountSupportAlias';
    private final String CONTACT_LAST_NAME = 'lastName';
    private final String CONTACT_EMAIL = '[email protected]';

    @testSetup
    private static void testSetup() {
        Account anAccount = new Account(
            Account_Support_Alias__c = ACCOUNT_SUPPORT_ALIAS)
        ;
        insert anAccount;

        Contact aContact = new Contact(
            LastName = CONTACT_LAST_NAME, 
            Email = CONTACT_EMAIL, 
            AccountId = anAccount.Id
        );
        insert aContact;
    }

    @isTest 
    private static void createUser_AccountFound_CreateUserForExistingContact() {
        final Id samlSsoProviderId = '0LE000000000000';
        final Id communityId = '0DB000000000000';
        final Id portalId = '0DB000000000000';
        final String federationIdentifier = 'federationIdentifier';
        final Map<String, String> attributes = new Map<String, String> {
            'AccountFederateId' => ACCOUNT_SUPPORT_ALIAS,
            'LastName' => CONTACT_LAST_NAME,
            'Email' => CONTACT_EMAIL
        };
        final String assertion = 'assertion';

        Test.startTest();
            BTCustomJITRegHandler handler = new BTCustomJITRegHandler();
            handler.createUser(samlSsoProviderId, communityId, portalId, federationIdentifier, attributes, assertion);
        Test.stopTest();

        // Select more fields and do more assertions than this
        User newUser = [SELECT Email, IsActive, CommunityNickname, FederationIdentifier FROM User];
        System.assertEquals(CONTACT_EMAIL, newUser.Email);
        System.assertEquals(true, newUser.IsActive);
        System.assert(CommunityNickname.startsWith('nickSSo'));
        System.assertEquals(federationIdentifier, newUser.FederationIdentifier);

        // No additional contacts created
        System.assertEquals(1, [SELECT count() FROM Contact]);
    }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .