[SalesForce] RestResponse does not return expected statuc code in Apex Rest unit test

Apex Rest returns 200 OK status code when directly access its Rest endpoint. However, in Apex Rest test class. RestResponse hooked in RestContext does not contains the status code (see below debug log).

Here is my test method. The containing class is also annotated with @isTest

@isTest
static void testCreateUser() {
    RestRequest req = new RestRequest();
    RestResponse res = new RestResponse();

    TestUtil.initTestData();

    User customerAdmin = [SELECT Id, AccountId FROM User WHERE username = 'testuser@acorp.com'];

    String requestBody = '{ "firstName": "Test", "lastName": "Case 2", "email": "testuser2@acorp.com" }';

    req.requestURI = URL.getSalesforceBaseUrl() + '/services/apexrest/MyId';
    req.httpMethod = 'POST';
    req.requestBody = Blob.valueOf(requestBody);

    Test.startTest();
    RestContext.request = req;
    RestContext.response = res;

    System.runAs(customerAdmin) {
        UserManagementRestController.createMyId();

        System.debug(req);
        System.debug(res);
    }

    Test.stopTest();
    // Something goes wrong here. No content in RestContext.response
    // Tempoary disable assertion on response
    //System.assertEquals(200, res.statusCode);

    List<User> users = [SELECT FirstName, LastName, Username, AccountId FROM User WHERE Email = 'testuser2@acorp.com'];
    System.assertEquals(1, users.size());

    User u = users[0];
    System.assertEquals('Test', u.FirstName);
    System.assertEquals('Case 2', u.LastName);
    System.assertEquals('testuser2@acorp.com', u.Username);
    System.assertEquals(customerAdmin.AccountId, u.AccountId);
}

My Apex REST class:

@RestResource(urlMapping='/MyId/*')
global class UserManagementRestController {

    @HttpPost
    global static void createMyId() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        User requestUser = [SELECT Id, AccountId, Email FROM User WHERE Id = :UserInfo.getUserId()];

        // check if reqest user is customer admin
        Set<String> userPermissions = new Set<String>(UserPermissionRemoter.getUserPermission(requestUser.Id));
        if(!userPermissions.contains(UserPermissionMapper.customerAdminPermissionName)) {
            responseBadRequest('Only Customer Admin can perform this action', res);
            return;
        }

        MyId hid;
        try {
            hid = (MyId)JSON.deserialize(req.requestBody.toString(), MyId.class);
        } catch (JSONException e) {
            responseBadRequest(e.getMessage(), res);
            return;
        }
        System.debug('hid:');
        System.debug(hid);

        String email = hid.email;

        if(!isValidateEmail(email)) {
            responseBadRequest('Invalidate Email', res);
            return;
        }

        // check if new email is in the same domain with super user
        String[] splitEmail = email.split('@');
        String[] splitCurrentUserEmail = requestUser.Email.split('@');
        if(splitEmail[1].compareTo(splitCurrentUserEmail[1]) != 0) {
            responseBadRequest('Email domain mismatched. The new user email must have the same domain as the customer admin’s email.', res);
            return;
        }

        Savepoint createNewUserSavePoint = Database.setSavepoint();
        try {
            Contact newCt = new Contact();
            newCt.FirstName = hid.firstName;
            newCt.LastName = hid.lastName;
            newCt.Email = hid.email;
            newCt.AccountId = requestUser.AccountId;
            insert newCt;

            // Directly use corporate email
            String username = newCt.Email;

            // gen nickname
            DateTime currentTime = System.now();
            Long timeInMili = currentTime.getTime()/1000;
            String nickname = splitEmail[0]+timeInMili;

            // gen alias
            String alias = username;
            if(alias.length() > 8) {
                alias = alias.substring(0, 8);
            }

            User newUsr = new User();
            newUsr.FirstName = newCt.firstName;
            newUsr.LastName = newCt.lastName;
            newUsr.Email = newCt.email;
            newUsr.CommunityNickname = nickname;
            newUsr.profileId = getCommunityProfileId();
            newUsr.contactId = newCt.Id;
            newUsr.UserName = username;
            newUsr.Alias = alias;
            newUsr.TimeZoneSidKey = 'America/Los_Angeles';
            newUsr.LocaleSidKey = 'en_US';
            newUsr.EmailEncodingKey = 'ISO-8859-1';
            newUsr.LanguageLocaleKey = 'en_US';
            newUsr.IsActive = true;

            insert newUsr;
        } catch (system.Dmlexception e) {
            Database.rollback(createNewUserSavePoint);
            if(String.valueOf(e).contains('DUPLICATE_USERNAME')||String.valueOf(e).contains('DUPLICATE')){
                responseBadRequest('An account with this email already exists', res);
                return;
            }
            else{
                responseBadRequest(String.valueOf(e), res);
                return;
            }
        }
    }

    private static Boolean isValidateEmail(String email) {
        //Pattern p = Pattern.compile('^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$');
        //Matcher m = p.matcher(email);

        // according to SFDC support: https://help.salesforce.com/apex/HTViewSolution?id=000170904&language=en_US
        Pattern p = Pattern.compile('^[A-Z0-9._%+-/!#$%&\'*=?^_`{|}~]+@[A-Z0-9.-]+\\.[A-Z]{2,4}$');
        Matcher m = p.matcher(email.toUpperCase());
        return m.matches();
    }

    private static Id getCommunityProfileId() {
        Profile p = [SELECT Id FROM Profile WHERE Name = 'My Customer Portal User' limit 1][0];
        return p.Id;
    }

    private static void responseBadRequest(String message, RestResponse res) {
        res.responseBody = Blob.valueOf(message);
        res.statusCode = 400;
        return;
    }

    public class MyId {
        public String firstName;
        public String lastName;
        public String email;

        public Boolean isCustomerAdmin;
        public Boolean isActive;
    }
}

In the debug log we can see the RestRequest is correctly set. However the RestResponse does not have any value in it. Would expect statusCode=200

21:56:59.902 (25902510196)|USER_DEBUG|[30]|DEBUG|RestRequest:[headers={}, httpMethod=POST, params={}, remoteAddress=null, requestBody=Blob[77], requestURI=Url:[delegate=http://cs1.salesforce.com]/services/apexrest/MyId, resourcePath=null]
21:56:59.902 (25902571034)|USER_DEBUG|[31]|DEBUG|RestResponse:[headers={}, responseBody=null, statusCode=null]

So what is the reason of getting different result from the test class and the actual Rest endpoint? Any way to test the exact behaviour of Apex Rest endpoint in a unit test class?

Best Answer

With the help of @Eric I finally figure out the problem. It is not related to System.runAs(). runAs() works well in the test case.

Root cause: The response from RestContext.reponse in Test Class is different from what we are getting through the Apex REST endpoint.

For example:

@RestResource(urlMapping='/MyRest/*')
global class MyRestController {
    @HttpGet
    global static String myRest() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        return 'From myRest';
    }
}

You will get

In Apex Test:

RestResponse:[headers={}, responseBody=null, statusCode=null]

When calling the Apex REST endpoint with REST client:

Status: 200 OK Body: "From myRest"

Salesforce Apex REST helps you to return status 200 and set the response body to the return String of myRest(). However this behaviour does not reflect in RestContext.response of Apex Rest test case. You have to explicitly set it in the code if you want to test the response...

@RestResource(urlMapping='/MyRest/*')
global class MyRestController {
    @HttpGet
    global static String myRest() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        // set the status code and response body for test case
        res.statusCode = 200;
        res.responseBody = Blob.valueOf('From myRest');

        return 'From myRest';
    }
}

You will get

In Apex Test:

RestResponse:[headers={}, responseBody=Blob[11], statusCode=200]
RestResponse.responseBody.toString():From myRest
Related Topic