Enterprise Java

Adding Social Sign In to a Spring MVC Web Application: Integration Testing

I have written about the challenges of writing unit tests for applications which use Spring Social 1.1.0 and provided one solution for it.

Although unit testing is valuable, it doesn’t really tell us if our application is working correctly.

That is why we have to write integration tests for it.

This blog post helps us to do that. During this blog post we will learn how we can write integration tests for the registration and login functions of our example application.

If you haven’t read the previous parts of my Spring Social tutorial, I recommend that you read them before reading this blog post. The prerequisites of this blog post are described in the following:

Let’s start by making some changes to the configuration of our build process.

Configuring Our Build Process

We have to make the following changes to the configuration of our build process:

  1. We have configure a local Maven repository and add Spring Test DbUnit 1.1.1 snapshot binaries to that repository.
  2. We have to add the required testing dependencies to our POM file.
  3. We have to add Liquibase changeset files to classpath.

Let’s find out how we can make these changes.

Adding Spring Test DBUnit Snapshot Binaries to Local Maven Repository

Because the stable version of Spring Test DBUnit isn’t compatible with Spring Framework 4, we must use the build snapshot in our integration tests.

We can add the Spring Test DBUnit snapshot to a local Maven repository by following these steps:

  1. Clone the Spring Test DBUnit repository from Github and create the snapshot binaries.
  2. Create etc/mavenrepo directory. This directory is our local Maven repository.
  3. Copy the created jar files to the etc/mavenrepo/com/github/springtestdbunit/1.1.1-SNAPSHOT directory.

After we have copied the jar files to our local Maven repository, we have to configure the location of the local repository in our pom.xml file. We can do this by adding the following repository declaration to our POM file:

<repositories>
    <!-- Other repositories are omitted for the sake of clarity -->
    <repository>
        <id>local-repository</id>
        <name>local repository</name>
        <url>file://${project.basedir}/etc/mavenrepo</url>
    </repository>
</repositories>

Getting the Required Testing Dependencies with Maven

We can get the required testing dependencies by adding the following dependency declaration to our POM file:

  • Spring Test DBUnit (version 1.1.1-SNAPSHOT). We use Spring Test DBUnit to integrate the Spring Test framework with the DbUnit library.
  • DbUnit (version 2.4.9). We use DbUnit to initialize our database into a known state before each integration test and verify that the contents of the database matches with the expected data.
  • liquibase-core (version 3.1.1). We use Liquibase to create some database tables when the application context of our integration tests is loaded.

The relevant part of our pom.xml file looks as follows:

<dependency>
    <groupId>com.github.springtestdbunit</groupId>
    <artifactId>spring-test-dbunit</artifactId>
    <version>1.1.1-SNAPSHOT</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.dbunit</groupId>
    <artifactId>dbunit</artifactId>
    <version>2.4.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.liquibase</groupId>
    <artifactId>liquibase-core</artifactId>
    <version>3.1.1</version>
    <scope>test</scope>
</dependency>

Adding Liquibase Changesets to the Classpath

Typically we should let Hibernate create the database which is used in our integration tests. However, this approach works only if every database table is configured in our domain model.

This is not the case now. The database of the example application has a UserConnection table which is not configured in the domain model of the example application. That is why we need to find another way to create the UserConnection table before our integration tests are run.

We can use the Spring integration of the Liquibase library for this purpose but this means that we have to add the Liquibase changesets to the classpath.

We can do this by using the Build Helper Maven plugin. We can add the the Liquibase changesets to the classpath by following these steps:

  1. Ensure that the add-test-resource goal of the Builder Helper Maven plugin is invoked at the generate-test-resources lifecycle phase.
  2. Configure the plugin to add the etc/db directory to the classpath (this directory contains the required files).

The relevant part of the plugin’s configuration looks as follows:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.7</version>
    <executions>
        <!-- Other executions are omitted for the sake of clarity -->
        <execution>
            <id>add-integration-test-resources</id>
            <!-- Run this execution in the generate-test-sources lifecycle phase -->
            <phase>generate-test-resources</phase>
            <goals>
                <!-- Invoke the add-test-resource goal of this plugin -->
                <goal>add-test-resource</goal>
            </goals>
            <configuration>
                <resources>
                    <!-- Other resources are omitted for the sake of clarity -->
                    <!-- Add the directory which contains Liquibase change sets to classpath -->
                    <resource>
                        <directory>etc/db</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

If want to get more information about the usage of the usage of the Builder Helper Maven plugin, you can take a look at the following webpages:

We have now finished the configuration of our build process. Let’s find out how we can configure our integration tests.

Configuring Our Integration Tests

We can configure our integration tests by following these steps:

  1. Modify the Liquibase changelog file.
  2. Configure the application context to run the Liquibase changesets before our test cases are invoked.
  3. Create a custom DbUnit dataset loader.
  4. Configure the integration test cases

Let’s move on and take a closer look at each step.

Modifying the Liquibase Changelog

Our example application has two Liquibase changesets which are found from the etc/db/schema directory. These changesets are:

  1. The db-0.0.1.sql file creates the UserConnection table which is used the persist the user’s connection to the used social sign in provider.
  2. The db-0.0.2.sql file creates the user_accounts table which contains the user accounts of our example application.

Because we want to run only the first changeset, we have to make some modifications to the Liquibase changelog file. To be more specific, we have to use Liquibase contexts to specify

  1. Which changesets are executed when we create the database of our example application.
  2. Which changesets are executed when we run our integration tests.

We can achieve our goal by following these steps:

  1. Specify that the db-0.0.1.sql changeset file is executed when the Liquibase context is either db or integrationtest.
  2. Specify that the db-0.0.2.sql changeset file is executed when the Liquibase context is db.

Our Liquibase changelog file looks as follows:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
       xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <!-- Run this change set when the database is created and integration tests are run -->
    <changeSet id="0.0.1" author="Petri" context="db,integrationtest">
        <sqlFile path="schema/db-0.0.1.sql" />
    </changeSet>

    <!-- Run this change set when the database is created -->
    <changeSet id="0.0.2" author="Petri" context="db">
        <sqlFile path="schema/db-0.0.2.sql" />
    </changeSet>
</databaseChangeLog>

Executing the Liquibase Changesets Before Integration Tests Are Run

We can execute Liquibase changesets before our integration tests are run by executing them when the application context is loaded. We can do this by following these steps:

      1. Create an IntegrationTestContext class and annotate it with the @Configuration annotation.
      2. Add a DataSource field to the created class and annotate it with the @Autowired annotation.
      3. Add a liquibase() method to the class and annotate it with the @Bean annotation. This method configures the the SpringLiquibase bean which executes the liquibase changesets when the application context is loaded.
      4. Implement the liquibase() method by following these steps:
        1. Create a new SpringLiquibase object.
        2. Configure the data source used by the created object.
        3. Configure the location of the Liquibase changelog.
        4. Set the Liquibase context to ‘integrationtest’.
        5. Return the created object.

The source code of the IntegrationTestContext class looks as follows:

import liquibase.integration.spring.SpringLiquibase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class IntegrationTestContext {

    @Autowired
    private DataSource dataSource;

    @Bean
    public SpringLiquibase liquibase() {
        SpringLiquibase liquibase = new SpringLiquibase();

        liquibase.setDataSource(dataSource);
        liquibase.setChangeLog("classpath:changelog.xml");
        liquibase.setContexts("integrationtest");

        return liquibase;
    }
}

Creating a Custom DataSetLoader Class

The DbUnit dataset which contains the information of different user accounts looks as follows:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <user_accounts id="1"
                  creation_time="2014-02-20 11:13:28"
                  email="facebook@socialuser.com"
                  first_name="Facebook"
                  last_name="User"
                  modification_time="2014-02-20 11:13:28"
                  role="ROLE_USER"
                  sign_in_provider="FACEBOOK"
                  version="0"/>
    <user_accounts id="2"
                  creation_time="2014-02-20 11:13:28"
                  email="twitter@socialuser.com"
                  first_name="Twitter"
                  last_name="User"
                  modification_time="2014-02-20 11:13:28"
                  role="ROLE_USER"
                  sign_in_provider="TWITTER"
                  version="0"/>
    <user_accounts id="3"
                  creation_time="2014-02-20 11:13:28"
                  email="registered@user.com"
                  first_name="RegisteredUser"
                  last_name="User"
                  modification_time="2014-02-20 11:13:28"
                  password="$2a$10$PFSfOaC2IFPG.1HjO05KoODRVSdESQ5q7ek4IyzVfTf.VWlKDa/.e"
                  role="ROLE_USER"
                  version="0"/>
   
    <UserConnection/>
</dataset>

We can see two things from this dataset:

  1. The users who created their user account by using social sign in don’t have a password.
  2. The user who created his user account by using normal registration has a password but he doesn’t have a sign in provider.

This is a problem because we use so called flat XML datasets and the default DbUnit dataset loader cannot handle this situation. We could of course start using the standard XML datasets but its syntax is a bit too verbose for my taste. That is why we have to create a custom dataset loader which can handle this situation.

We can create a custom dataset loader by following these steps:

  1. Create a ColumnSensingFlatXMLDataSetLoader class which extends the AbstractDataSetLoader class.
  2. Override the createDataSet() method and implement it by following these steps:
    1. Create a new FlatXmlDataSetBuilder object.
    2. Enable column sensing. Column sensing means that DbUnit reads the entire dataset from the dataset file and adds new columns when they are found from the dataset. This ensures that the value of every column is inserted correctly to the database.
    3. Create a new IDataSet object and return the created object.

The source code of the ColumnSensingFlatXMLDataSetLoader class looks as follows:

import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.springframework.core.io.Resource;

import java.io.InputStream;

public class ColumnSensingFlatXMLDataSetLoader extends AbstractDataSetLoader {
    @Override
    protected IDataSet createDataSet(Resource resource) throws Exception {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        builder.setColumnSensing(true);
        InputStream inputStream = resource.getInputStream();
        try {
            return builder.build(inputStream);
        } finally {
            inputStream.close();
        }
    }
}

However, creating a custom dataset loader class isn’t enough. We still have to configure our tests to use this class when our datasets are loaded. We can do this by annotating the test class with the @DbUnitConfiguration annotation and setting the value of its dataSetLoader attribute to ColumnSensingFlatXMLDataSetLoader.class.

Let’s move on see how this is done.

Configuring Our Integration Tests

We can configure our integration tests by following these steps:

  1. Ensure that tests are executed by the Spring SpringJUnit4ClassRunner. We can do this by annotating the test class with the @RunWith annotation and setting its value to SpringJUnit4ClassRunner.class.
  2. Load the application context by annotating the test class with the @ContextConfiguration annotation, and configure the used application context configuration classes or files.
  3. Annotate the test class with the @WebAppConfiguration annotation. This ensures that the application context loaded for our integration tests is a WebApplicationContext.
  4. Annotate the class with the @TestExecutionListeners annotation and pass the standard Spring listeners and the DBUnitTestExecutionListener as its value. The DBUnitTestExecutionListener ensures that Spring processes the DbUnit annotations found from our test class.
  5. Configure the test class to user our custom dataset loader by annotating the test class with the @DbUnitConfiguration annotation. Set the value of its dataSetLoader attribute to ColumnSensingFlatXMLDataSetLoader.class.
  6. Add a FilterChainProxy field to the test class and annotate the field with the @Autowired annotation.
  7. Add a WebApplicationContext field to the test class and annotate the field with the @Autowired annotation.
  8. Add a MockMvc field to the test class.
  9. Add a setUp() method to the test class and annotate that method with the @Before annotation which ensures that this method is invoked before each test method.
  10. Implement the setUp() method and create a new MockMvc object by using the MockMvcBuilders class.

The source code of an empty test class looks as follows:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ExampleApplicationContext.class, IntegrationTestContext.class})
//@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"})
@WebAppConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITTest {

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .addFilter(springSecurityFilterChain)
                .build();
    }
}

If you need more information about the configuration of our integration tests, I recommend that you read the following blog posts:

We have now learned how we can configure our integration tests. Let’s move on and create some testing utility classes which are used in our integration tests.

Creating Testing Utility Classes

Next we will create three utility classes which are used in our integration tests:

  1. We will create the IntegrationTestConstants class which contains the constants used in more than one integration test.
  2. We will create the classes which are used to create ProviderSignInAttempt objects for our integration tests.
  3. We will create a test data builder class which is used to create CsrfToken objects.

Let’s find out why we have to create these classes and how we can create them.

Creating the IntegrationTestConstants Class

When we write integration (or unit) tests, sometimes we need to use the same information in many test classes. Duplicating this information to all tests classes is a bad idea because it makes our tests harder to maintain and understand. Instead we should put this information to a single class and get it from that class when we need it.

The IntegrationTestConstants class contains the following information which is used in more than one test class:

  • It has the constants which are related to the CSRF protection of Spring Security 3.2. These constants include: the name of the HTTP header which contains the CSRF token, the name of the request parameter which contains the value of the CSRF token, the name of the session attribute which contains the CsrfToken object, and the value of the CSRF token.
  • It contains the User enum which specifies the users used in our integration test. Each user has an username and a password (this is not required). The information of this enum is used for two purposes:
    1. It is used to specify the logged in user. This is useful when we are integration tests for protected functions (functions that require some kind of authorization).
    2. When we write integration tests for the login function, we need to specify the username and password of the user who trying to log in to the application.

The source code of the IntegrationTestConstants class looks as follows:

import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;

public class IntegrationTestConstants {

    public static final String CSRF_TOKEN_HEADER_NAME = "X-CSRF-TOKEN";
    public static final String CSRF_TOKEN_REQUEST_PARAM_NAME = "_csrf";
    public static final String CSRF_TOKEN_SESSION_ATTRIBUTE_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
    public static final String CSRF_TOKEN_VALUE = "f416e226-bebc-401d-a1ed-f10f47aa9c56";

    public enum User {

        FACEBOOK_USER("facebook@socialuser.com", null),
        REGISTERED_USER("registered@user.com", "password"),
        TWITTER_USER("twitter@socialuser.com", null);

        private String password;

        private String username;

        private User(String username, String password) {
            this.password = password;
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public String getUsername() {
            return username;
        }
    }
}

Creating ProviderSignInAttempt Objects

When we wrote unit tests for our example application we took a quick look at the ProviderSignInUtils class and realized that we have to find a way to create ProviderSignInAttempt objects.

We solved that problem by creating a stub class which was used in our unit tests. This stub class gives us the possibility to configure the returned Connection<?> object and to verify that a specific connection was “persisted to the database”. However, our stub class didn’t persist connections to the used database. Instead it stored the user id of the user to a Set object.

Because now we want to persist connection data to the database, we have to make changes to the our stub class. We can make these changes by making these changes to the TestProviderSignInAttempt object:

      1. Add a private usersConnectionRepositorySet field to the TestProviderSignInAttempt class. The type of this field is boolean and its default value is false. This field describes if we can persist connections to the used data storage.
      2. Add a new constructor argument to the constructor of the TestProviderSignInAttempt class. The type of this argument is UsersConnectionRepository and it is used to persist connections to the used data storage.
      3. Implement the constructor by following these steps:
        1. Call the constructor of the super class and pass the Connection<?> and UsersConnectionRepository objects as constructor arguments.
        2. Store a reference to the Connection<?> object given as a constructor argument to the connection field.
        3. If the UsersConnectionRepository object given as a constructor argument isn’t null, set the value of the usersConnectionRepositoryField to true.
      4. Implement the addConnection() method by following these steps:
        1. Add the user id given as a method parameter to the connections Set.
        2. If the UsersConnectionRepository object was set when a new TestProviderSignInAttempt object was created, call the addConnection() method of the ProviderSignInAttempt class and pass the user id as a method parameter.

    The source code of the TestProviderSignInAttempt class looks as follows (the modified parts are highlighted):

    import org.springframework.social.connect.Connection;
    import org.springframework.social.connect.UsersConnectionRepository;
    
    import java.util.HashSet;
    import java.util.Set;
    
    public class TestProviderSignInAttempt extends ProviderSignInAttempt {
    
        private Connection<?> connection;
    
        private Set<String> connections = new HashSet<>();
    
        private boolean usersConnectionRepositorySet = false;
    
        public TestProviderSignInAttempt(Connection<?> connection, UsersConnectionRepository usersConnectionRepository) {
            super(connection, null, usersConnectionRepository);
            this.connection = connection;
    
            if (usersConnectionRepository != null) {
                this.usersConnectionRepositorySet = true;
            }
        }
    
        @Override
        public Connection<?> getConnection() {
            return connection;
        }
    
        @Override
        void addConnection(String userId) {
            connections.add(userId);
            if (usersConnectionRepositorySet) {
                super.addConnection(userId);
            }
        }
    
        public Set<String> getConnections() {
            return connections;
        }
    }

    Because we build new TestProviderSignInAttempt objects by using the TestProviderSignInAttemptBuilder, we have to make changes to that class as well. We can make these changes by following these steps:

    1. Add a private usersConnectionRepository field to the TestProviderSignInAttemptBuilder class and set its type to UsersConnectionRepository.
    2. Add a usersConnectionRepository() method to the class. Set a reference to UsersConnectionRepository object to the usersConnectionRepository field and return a reference to the builder object.
    3. Modify the last the line of the build() method and create a new TestProviderSignInAttempt object by using the new constructor which we created earlier.

    The source code of the TestProviderSignInAttemptBuilder class looks as follows (the modified parts are highlighted):

    import org.springframework.social.connect.*;
    import org.springframework.social.connect.web.TestProviderSignInAttempt;
    
    public class TestProviderSignInAttemptBuilder {
    
        private String accessToken;
    
        private String displayName;
    
        private String email;
    
        private Long expireTime;
    
        private String firstName;
    
        private String imageUrl;
    
        private String lastName;
    
        private String profileUrl;
    
        private String providerId;
    
        private String providerUserId;
    
        private String refreshToken;
    
        private String secret;
    
        private UsersConnectionRepository usersConnectionRepository;
    
        public TestProviderSignInAttemptBuilder() {
    
        }
    
        public TestProviderSignInAttemptBuilder accessToken(String accessToken) {
            this.accessToken = accessToken;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder connectionData() {
            return this;
        }
    
        public TestProviderSignInAttemptBuilder displayName(String displayName) {
            this.displayName = displayName;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder email(String email) {
            this.email = email;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder expireTime(Long expireTime) {
            this.expireTime = expireTime;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder imageUrl(String imageUrl) {
            this.imageUrl = imageUrl;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder profileUrl(String profileUrl) {
            this.profileUrl = profileUrl;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder providerId(String providerId) {
            this.providerId = providerId;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder providerUserId(String providerUserId) {
            this.providerUserId = providerUserId;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder refreshToken(String refreshToken) {
            this.refreshToken = refreshToken;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder secret(String secret) {
            this.secret = secret;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder usersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
            this.usersConnectionRepository = usersConnectionRepository;
            return this;
        }
    
        public TestProviderSignInAttemptBuilder userProfile() {
            return this;
        }
    
        public TestProviderSignInAttempt build() {
            ConnectionData connectionData = new ConnectionData(providerId,
                    providerUserId,
                    displayName,
                    profileUrl,
                    imageUrl,
                    accessToken,
                    secret,
                    refreshToken,
                    expireTime);
    
            UserProfile userProfile = new UserProfileBuilder()
                    .setEmail(email)
                    .setFirstName(firstName)
                    .setLastName(lastName)
                    .build();
    
            Connection connection = new TestConnection(connectionData, userProfile);
    
            return new TestProviderSignInAttempt(connection, usersConnectionRepository);
        }
    }

    Creating CsrfToken objects

    Because our example application uses CSRF protection provided by Spring Security 3.2, we have to figure out a way create valid CSRF tokens in our integration tests. The CsrfToken interface declares the methods which provide information about the an expected CSRF token. This interface has one implementation called the DefaultCsrfToken.

    In other words, we have to figure out a way to create new DefaultCsrfToken objects. The DefaultCsrfToken class has a single constructor, and we could of course use it when we create new DefaultCsrfToken objects in our integration tests. The problem is that this isn’t very readable.

    Instead, we will create a test data builder class which provides a fluent API for creating new CsrfToken objects. We can create this class by following these steps:

    1. Create a CsrfTokenBuilder class.
    2. Add a private headerName field to the created class.
    3. Add a private requestParameterName field to the created class.
    4. Add a private tokenValue field to the created class.
    5. Add a publish constructor to the created class.
    6. Add the methods used to set the field values of headerName, requestParameterName, and tokenValue fields.
    7. Add a build() method to the created class and set its return type to CsrfToken. Implement this method by following these steps:
      1. Create a new DefaultCsrfToken object and provide the name of the CSRF token header, the name of the CSRF token request parameter, and the value of the CSRF token as constructor arguments.
      2. Return the created object.

    The source code of the CsrfTokenBuilder class looks as follows:

    import org.springframework.security.web.csrf.CsrfToken;
    import org.springframework.security.web.csrf.DefaultCsrfToken;
    
    public class CsrfTokenBuilder {
    
        private String headerName;
        private String requestParameterName;
        private String tokenValue;
    
        public CsrfTokenBuilder() {
    
        }
    
        public CsrfTokenBuilder headerName(String headerName) {
            this.headerName = headerName;
            return this;
        }
    
        public CsrfTokenBuilder requestParameterName(String requestParameterName) {
            this.requestParameterName = requestParameterName;
            return this;
        }
    
        public CsrfTokenBuilder tokenValue(String tokenValue) {
            this.tokenValue = tokenValue;
            return this;
        }
    
        public CsrfToken build() {
            return new DefaultCsrfToken(headerName, requestParameterName, tokenValue);
        }
    }

    We can create new CsrfToken objects by using this code:

    CsrfToken csrfToken = new CsrfTokenBuilder()
            .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
            .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
            .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
            .build();

    We have now created the required testing utility classes. Let’s move on and start writing integration tests for our example application.

    Writing Integration Tests

    We are finally ready to write some integration tests for our example application. We will write the following integration tests:

    • We will write integration tests which ensure that the form login is working correctly.
    • We will write integration tests which verifies that the registration is working correctly when social sign in is used.

    But before we will start writing these integration tests, we will learn how we can provide valid CSRF tokens to Spring Security.

    Providing Valid CSRF Tokens to Spring Security

    Earlier we learned how we can create CsrfToken objects in our integration tests. However, we still have to figure out a way to provide these CSRF tokens to Spring Security.

    It is time to a take a closer look at the way Spring Security handles CSRF tokens.

    The CsrfTokenRepository interface declares the methods which are required to generate, save and load CSRF tokens. The default implementation of this interface is the HttpSessionCsrfTokenRepository class which stores CSRF tokens to HTTP session.

    We need to find the answers to the following questions:

    • How the CSRF tokens are saved to the HTTP session?
    • How the CSRF tokens are loaded from the HTTP session?

    We can found answers to these questions by taking a look at the source code of the HttpSessionCsrfTokenRepository class. The relevant part of the HttpSessionCsrfTokenRepository class looks as follows:

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
    
        private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
    
        private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
    
        public void saveToken(CsrfToken token, HttpServletRequest request,
                HttpServletResponse response) {
            if (token == null) {
                HttpSession session = request.getSession(false);
                if (session != null) {
                    session.removeAttribute(sessionAttributeName);
                }
            } else {
                HttpSession session = request.getSession();
                session.setAttribute(sessionAttributeName, token);
            }
        }
    
        public CsrfToken loadToken(HttpServletRequest request) {
            HttpSession session = request.getSession(false);
            if (session == null) {
                return null;
            }
            return (CsrfToken) session.getAttribute(sessionAttributeName);
        }
        
        //Other methods are omitted.
    }

    It is now clear that the CSRF token are stored to the HTTP session as CsrfToken objects, and these objects are retried and stored by using the value of the sessionAttributeName property. This means that if we want to provide a valid CSRF token to Spring Security, we have to follow these steps:

    1. Create a new CsrfToken object by using our test data builder.
    2. Send the value of the CSRF token as a request parameter.
    3. Store the created DefaultCsrfToken object to HTTP session so that the HttpSessionCsrfTokenRepository finds it.

    The source code of our dummy test looks as follows:

    import org.junit.Test;
    import org.springframework.security.web.csrf.CsrfToken;
    import org.springframework.security.web.csrf.DefaultCsrfToken;
    import org.springframework.test.web.servlet.MockMvc;
    
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    
    public class ITCSRFTest {
    
        private MockMvc mockMvc;
    
        @Test
        public void test() throws Exception {
            //1. Create a new CSRF token
            CsrfToken csrfToken = new CsrfTokenBuilder()
                    .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
                    .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
                    .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
                    .build();
    
            mockMvc.perform(post("/login/authenticate")
                    //2. Send the value of the CSRF token as request parameter
                    .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE)
                    
                    //3. Set the created CsrfToken object to session so that the CsrfTokenRepository finds it
                    .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken)
            )
                    //Add assertions here.
        }
    }

    Enough with theory. We are now ready to write some integration tests for our application. Let’s start by writing integration to the login function of our example application.

    Writing Tests for the Login Function

    It is time to write integration tests for the login function of our example application. We will write the following integration tests for it:

    1. We will write an integration test which ensures that everything is working as expected when login is successful.
    2. We will write an integration test which ensures that everything is working when login fails.

    Both of these integration tests initializes the database into to a known state by using the same DbUnit dataset file (users.xml) and its content looks as follows:

    <?xml version='1.0' encoding='UTF-8'?>
    <dataset>
        <user_accounts id="1"
                      creation_time="2014-02-20 11:13:28"
                      email="facebook@socialuser.com"
                      first_name="Facebook"
                      last_name="User"
                      modification_time="2014-02-20 11:13:28"
                      role="ROLE_USER"
                      sign_in_provider="FACEBOOK"
                      version="0"/>
        <user_accounts id="2"
                      creation_time="2014-02-20 11:13:28"
                      email="twitter@socialuser.com"
                      first_name="Twitter"
                      last_name="User"
                      modification_time="2014-02-20 11:13:28"
                      role="ROLE_USER"
                      sign_in_provider="TWITTER"
                      version="0"/>
        <user_accounts id="3"
                      creation_time="2014-02-20 11:13:28"
                      email="registered@user.com"
                      first_name="RegisteredUser"
                      last_name="User"
                      modification_time="2014-02-20 11:13:28"
                      password="$2a$10$PFSfOaC2IFPG.1HjO05KoODRVSdESQ5q7ek4IyzVfTf.VWlKDa/.e"
                      role="ROLE_USER"
                      version="0"/>
       
        <UserConnection/>
    </dataset>

    Let’s get started.

    Test 1: Login is Successful

    We can write the first integration test by following these steps:

    1. Annotate the test class with the @DatabaseSetup annotation and configure the dataset which is used to initialize the database into a known state before the integration test is invoked.
    2. Create a new CsrfToken object.
    3. Send a POST request to the url ‘/login/authenticate’ by following these steps:
      1. Set the values of username and password request parameters. Use correct password.
      2. Set the value of the CSRF token to the request.
      3. Set the created CsrfToken to session.
    4. ensure that the HTTP status code 302 is returned.
    5. Verify that the the request is redirected to url ‘/’.

The source code of our integration test looks as follows:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ExampleApplicationContext.class, IntegrationTestContext.class})
//@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"})
@WebAppConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
@DatabaseSetup("/net/petrikainulainen/spring/social/signinmvc/user/users.xml")
public class ITFormLoginTest {

    private static final String REQUEST_PARAM_PASSWORD = "password";
    private static final String REQUEST_PARAM_USERNAME = "username";

    //Some fields are omitted for the sake of clarity

    private MockMvc mockMvc;

    //The setUp() method is omitted for the sake of clarify.

    @Test
    public void login_CredentialsAreCorrect_ShouldRedirectUserToFrontPage() throws Exception {
        CsrfToken csrfToken = new CsrfTokenBuilder()
                .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
                .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
                .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .build();

        mockMvc.perform(post("/login/authenticate")
                .param(REQUEST_PARAM_USERNAME, IntegrationTestConstants.User.REGISTERED_USER.getUsername())
                .param(REQUEST_PARAM_PASSWORD, IntegrationTestConstants.User.REGISTERED_USER.getPassword())
                .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken)
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/"));
    }
}

Test 2: Login Fails

We can write the second integration test by following these steps:

  1. Annotate the test class with the @DatabaseSetup annotation and configure the dataset which is used to initialize the database into a known state before the integration test is invoked.
  2. Create a new CsrfToken object.
  3. Send a POST request to the url ‘/login/authenticate’ by following these steps:
    1. Set values of username and password request parameters. Use incorrect password.
    2. Set the value of the CSRF token to the request as a request parameter.
    3. Set the created CsrfToken object to session.
  4. Ensure that the HTTP status code 302 is returned.
  5. Verify that the request is redirected to the url ‘/login?error=bad_credentials’.

The source code of our integration test looks as follows:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ExampleApplicationContext.class, IntegrationTestContext.class})
//@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"})
@WebAppConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
@DatabaseSetup("/net/petrikainulainen/spring/social/signinmvc/user/users.xml")
public class ITFormLoginTest {

    private static final String REQUEST_PARAM_PASSWORD = "password";
    private static final String REQUEST_PARAM_USERNAME = "username";

    //Some fields are omitted for the sake of clarity

    private MockMvc mockMvc;

    //The setUp() method is omitted for the sake of clarify.

    @Test
    public void login_InvalidPassword_ShouldRedirectUserToLoginForm() throws Exception {
        CsrfToken csrfToken = new CsrfTokenBuilder()
                .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
                .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
                .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .build();

        mockMvc.perform(post("/login/authenticate")
                .param(REQUEST_PARAM_USERNAME, IntegrationTestConstants.User.REGISTERED_USER.getUsername())
                .param(REQUEST_PARAM_PASSWORD, "invalidPassword")
                .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken)
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/login?error=bad_credentials"));
    }
}

Writing Tests for the Registration Function

We will write the following integration tests for the registration function:

  1. We will write an integration test which ensures that our application is working properly when the user is creating new user account by using social sign in but the validation of the submitted registration form fails.
  2. We will write an integration test which verifies that everything is working correctly when the user is creating a new user account by using social sign in and an email address which is found from the database.
  3. We will write an integration test which ensures that it is possible to create a new user account by using social sign in.

Let’s get started.

Test 1: Validation Fails

We can write the first integration test by following these steps:

  1. Add a UsersConnectionRepository field to the test class and annotate it with the @Autowired annotation.
  2. Annotate the test method with the @DatabaseSetup annotation and configure the dataset which is used to initialize the database into a known state before our integration test is run.
  3. Create a new TestProviderSignInAttempt object. Remember to set the used UsersConnectionRepository object.
  4. Create a new RegistrationForm object and set the value of its signInProvider field.
  5. Create a new CsrfToken object.
  6. Send a POST request to the url ‘/user/register’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Convert the form object to url encoded bytes and set it to the body of the request.
    3. Set the created TestProviderSignInAttempt object to session.
    4. Set the value of the CSRF token to the request as a request parameter.
    5. Set the created CsrfToken object to session.
    6. Set the created form object to session.
  7. Ensure that the HTTP request status 200 is returned.
  8. Ensure that the name of the rendered view is ‘user/registrationForm’.
  9. Verify that the request is forwarded to the url ‘/WEB-INF/jsp/user/registrationForm.jsp’.
  10. Verify that the fields of the model attribute called ‘user’ are correct.
  11. Ensure that the model attribute called ‘user’ has field errors in email, firstName and lastName fields.
  12. Annotate the test method with the @ExpectedDatabase annotation and ensure that new user account wasn’t saved to the database (use the same dataset which was used to initialize the database).

The source code of our integration test looks as follows:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;

import static net.petrikainulainen.spring.social.signinmvc.user.controller.TestProviderSignInAttemptAssert.assertThatSignIn;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ExampleApplicationContext.class, IntegrationTestContext.class})
//@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"})
@WebAppConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITRegistrationControllerTest {
    
    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    //Some fields are omitted for the sake of clarity.

    private MockMvc mockMvc;

    //The setUp() is omitted for the sake of clarity.

    @Test
    @DatabaseSetup("no-users.xml")
    @ExpectedDatabase(value="no-users.xml", assertionMode = DatabaseAssertionMode.NON_STRICT)
    public void registerUserAccount_SocialSignInAndEmptyForm_ShouldRenderRegistrationFormWithValidationErrors() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .accessToken("accessToken")
                    .displayName("John Smith")
                    .expireTime(100000L)
                    .imageUrl("https://www.twitter.com/images/johnsmith.jpg")
                    .profileUrl("https://www.twitter.com/johnsmith")
                    .providerId("twitter")
                    .providerUserId("johnsmith")
                    .refreshToken("refreshToken")
                    .secret("secret")
                .usersConnectionRepository(usersConnectionRepository)
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        CsrfToken csrfToken = new CsrfTokenBuilder()
                .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
                .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
                .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .build();

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken)
                .sessionAttr("user", userAccountData)
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", isEmptyOrNullString()),
                        hasProperty("firstName", isEmptyOrNullString()),
                        hasProperty("lastName", isEmptyOrNullString()),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is(SocialMediaService.TWITTER))
                )))
                .andExpect(model().attributeHasFieldErrors("user", "email", "firstName", "lastName"));
    }
}

Our integration test uses a DbUnit dataset file called no-users.xml which looks as follows:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <user_accounts/>
    <UserConnection/>
</dataset>

Test 2: Email Address Is Found From the Database

We can write the second integration test by following these steps:

  1. Add a UsersConnectionRepository field to the test class and annotate it with the @Autowired annotation.
  2. Annotate the test method with the @DatabaseSetup annotation and configure the dataset which is used to initialize the database into a known state before our integration test is run.
  3. Create a new TestProviderSignInAttempt object. Remember to set the used UsersConnectionRepository object.
  4. Create a new RegistrationForm object and set the values of its email, firstName, lastName, and signInProvider fields. Use an existing email address.
  5. Create a new CsrfToken object.
  6. Send a POST request to the url ‘/user/register’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Convert the form object to url encoded bytes and set it to the body of the request.
    3. Set the created TestProviderSignInAttempt object to session.
    4. Set the value of the CSRF token to the request as a request parameter.
    5. Set the created CsrfToken object to session.
    6. Set the created form object to session.
  7. Ensure that the HTTP request status 200 is returned.
  8. Ensure that the name of the rendered view is ‘user/registrationForm’.
  9. Verify that the request is forwarded to the url ‘/WEB-INF/jsp/user/registrationForm.jsp’.
  10. Verify that the fields of the model attribute called ‘user’ are correct.
  11. Ensure that the model attribute called ‘user’ has a field error in email field.
  12. Annotate the test method with the @ExpectedDatabase annotation and ensure that new user account wasn’t saved to the database (use the same dataset which was used to initialize the database).

The source code of our integration test looks as follows:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ExampleApplicationContext.class, IntegrationTestContext.class})
//@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"})
@WebAppConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITRegistrationControllerTest {

    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    //Some fields are omitted for the sake of clarity.

    private MockMvc mockMvc;

    //The setUp() is omitted for the sake of clarity.

    @Test
    @DatabaseSetup("/net/petrikainulainen/spring/social/signinmvc/user/users.xml")
    @ExpectedDatabase(value = "/net/petrikainulainen/spring/social/signinmvc/user/users.xml", assertionMode = DatabaseAssertionMode.NON_STRICT)
    public void registerUserAccount_SocialSignInAndEmailExist_ShouldRenderRegistrationFormWithFieldError() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .accessToken("accessToken")
                    .displayName("John Smith")
                    .expireTime(100000L)
                    .imageUrl("https://www.twitter.com/images/johnsmith.jpg")
                    .profileUrl("https://www.twitter.com/johnsmith")
                    .providerId("twitter")
                    .providerUserId("johnsmith")
                    .refreshToken("refreshToken")
                    .secret("secret")
                .usersConnectionRepository(usersConnectionRepository)
                .userProfile()
                    .email(IntegrationTestConstants.User.REGISTERED_USER.getUsername())
                    .firstName("John")
                    .lastName("Smith")
                .build();

        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .email(IntegrationTestConstants.User.REGISTERED_USER.getUsername())
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        CsrfToken csrfToken = new CsrfTokenBuilder()
                .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
                .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
                .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .build();

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken)
                .sessionAttr("user", userAccountData)
        )
                .andExpect(status().isOk())
                .andExpect(view().name("user/registrationForm"))
                .andExpect(forwardedUrl("/WEB-INF/jsp/user/registrationForm.jsp"))
                .andExpect(model().attribute("user", allOf(
                        hasProperty("email", is(IntegrationTestConstants.User.REGISTERED_USER.getUsername())),
                        hasProperty("firstName", is("John")),
                        hasProperty("lastName", is("Smith")),
                        hasProperty("password", isEmptyOrNullString()),
                        hasProperty("passwordVerification", isEmptyOrNullString()),
                        hasProperty("signInProvider", is(SocialMediaService.TWITTER))
                )))
                .andExpect(model().attributeHasFieldErrors("user", "email"));
    }
}

This integration test uses a DbUnit dataset called users.xml which looks as follows:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <user_accounts id="1" 
                  creation_time="2014-02-20 11:13:28" 
                  email="facebook@socialuser.com" 
                  first_name="Facebook" 
                  last_name="User" 
                  modification_time="2014-02-20 11:13:28" 
                  role="ROLE_USER" 
                  sign_in_provider="FACEBOOK" 
                  version="0"/>
    <user_accounts id="2" 
                  creation_time="2014-02-20 11:13:28" 
                  email="twitter@socialuser.com" 
                  first_name="Twitter" 
                  last_name="User" 
                  modification_time="2014-02-20 11:13:28" 
                  role="ROLE_USER" 
                  sign_in_provider="TWITTER" 
                  version="0"/>
    <user_accounts id="3" 
                  creation_time="2014-02-20 11:13:28" 
                  email="registered@user.com" 
                  first_name="RegisteredUser" 
                  last_name="User" 
                  modification_time="2014-02-20 11:13:28" 
                  password="$2a$10$PFSfOaC2IFPG.1HjO05KoODRVSdESQ5q7ek4IyzVfTf.VWlKDa/.e" 
                  role="ROLE_USER" 
                  version="0"/>
    
    <UserConnection/>
</dataset>

Test 3: Registration Is Successful

We can write the third integration test by following these steps:

  1. Add a UsersConnectionRepository field to the test class and annotate it with the @Autowired annotation.
  2. Annotate the test method with the @DatabaseSetup annotation and configure the dataset which is used to initialize the database into a known state before our integration test is run.
  3. Create a new TestProviderSignInAttempt object. Remember to set the used UsersConnectionRepository object.
  4. Create a new RegistrationForm object and set set the values of its email, firstName, lastName, and signInProvider fields.
  5. Create a new CsrfToken object.
  6. Send a POST request to the url ‘/user/register’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Convert the form object to url encoded bytes and set it to the body of the request.
    3. Set the created TestProviderSignInAttempt object to session.
    4. Set the value of the CSRF token to the request as a request parameter.
    5. Set the created CsrfToken object to session.
    6. Set the created form object to session.
  7. Ensure that the HTTP request status 302 is returned.
  8. Verify that the request is redirected to the url ‘/’. This also ensures that the created user is signed in because anonymous users cannot access that url.
  9. Annotate the test method with the @ExpectedDatabase annotation and ensure that a new user account was saved to a database and the connection to the used social media provider was persisted.

The source code of our integration test looks as follows:

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.support.TestProviderSignInAttemptBuilder;
import org.springframework.social.connect.web.ProviderSignInAttempt;
import org.springframework.social.connect.web.TestProviderSignInAttempt;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ExampleApplicationContext.class, IntegrationTestContext.class})
//@ContextConfiguration(locations = {"classpath:exampleApplicationContext.xml"})
@WebAppConfiguration
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class })
@DbUnitConfiguration(dataSetLoader = ColumnSensingFlatXMLDataSetLoader.class)
public class ITRegistrationControllerTest2 {
    
    @Autowired
    private UsersConnectionRepository usersConnectionRepository;

    //Some fields are omitted for the sake of clarity.

    private MockMvc mockMvc;

    //The setUp() is omitted for the sake of clarity.

    @Test
    @DatabaseSetup("no-users.xml")
    @ExpectedDatabase(value="register-social-user-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT)
    public void registerUserAccount_SocialSignIn_ShouldCreateNewUserAccountAndRenderHomePage() throws Exception {
        TestProviderSignInAttempt socialSignIn = new TestProviderSignInAttemptBuilder()
                .connectionData()
                    .accessToken("accessToken")
                    .displayName("John Smith")
                    .expireTime(100000L)
                    .imageUrl("https://www.twitter.com/images/johnsmith.jpg")
                    .profileUrl("https://www.twitter.com/johnsmith")
                    .providerId("twitter")
                    .providerUserId("johnsmith")
                    .refreshToken("refreshToken")
                    .secret("secret")
                .usersConnectionRepository(usersConnectionRepository)
                .userProfile()
                    .email("john.smith@gmail.com")
                    .firstName("John")
                    .lastName("Smith")
                .build();

        RegistrationForm userAccountData = new RegistrationFormBuilder()
                .email("john.smith@gmail.com")
                .firstName("John")
                .lastName("Smith")
                .signInProvider(SocialMediaService.TWITTER)
                .build();

        CsrfToken csrfToken = new CsrfTokenBuilder()
                .headerName(IntegrationTestConstants.CSRF_TOKEN_HEADER_NAME)
                .requestParameterName(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME)
                .tokenValue(IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .build();

        mockMvc.perform(post("/user/register")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .content(TestUtil.convertObjectToFormUrlEncodedBytes(userAccountData))
                .sessionAttr(ProviderSignInAttempt.SESSION_ATTRIBUTE, socialSignIn)
                .param(IntegrationTestConstants.CSRF_TOKEN_REQUEST_PARAM_NAME, IntegrationTestConstants.CSRF_TOKEN_VALUE)
                .sessionAttr(IntegrationTestConstants.CSRF_TOKEN_SESSION_ATTRIBUTE_NAME, csrfToken)
                .sessionAttr("user", userAccountData)
        )
                .andExpect(status().isMovedTemporarily())
                .andExpect(redirectedUrl("/"));
    }
}

The dataset (no-users.xml) which is used to initialize the database into a known state looks as follows:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <user_accounts/>
    <UserConnection/>
</dataset>

The DbUnit dataset called register-social-user-expected.xml is used to verify that the user account was created successfully and the connection to the used social sign in provider was persisted to the database. It looks as follows:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <user_accounts email="john.smith@gmail.com" 
                  first_name="John" last_name="Smith" 
                  role="ROLE_USER" 
                  sign_in_provider="TWITTER"
                  version="0"/>

    <UserConnection userId="john.smith@gmail.com"
                   providerId="twitter"
                   providerUserId="johnsmith"
                   rank="1"
                   displayName="John Smith"
                   profileUrl="https://www.twitter.com/johnsmith"
                   imageUrl="https://www.twitter.com/images/johnsmith.jpg"
                   accessToken="accessToken"
                   secret="secret"
                   refreshToken="refreshToken"
                   expireTime="100000"/>
</dataset>

Summary

We have now learned how we can write integration tests for a normal Spring MVC application which uses Spring Social 1.1.0. This tutorial has taught us many things but these two things are the key lessons of this blog post:

  • We learned how we can “simulate” social sign in by creating ProviderSignInAttempt objects and using them in our integration tests.
  • We learned how we can create CSRF tokens and provide the created tokens to Spring Security.

Let’s spend a moment and analyze the pros and cons of the approach described in this blog post:

Pros:

  • We can write integration tests without using an external social sign in provider. This makes our tests less brittle and easier to maintain.
  • The implementation details of Spring Social (ProviderSignInAttempt) and Spring Security CSRF protection (CsrfToken) are “hidden” to test data builder classes. This makes our tests more readable and easier to maintain.

Cons:

  • This tutorial doesn’t describe how we can write integration tests for social sign in (login using a social sign in provider). I tried to figure out a way to write these tests without using an external sign in provider but I simply ran out of time (it seemed complicated and I wanted to publish this blog post).

This blog post ends my ‘Adding Social Sign in to a Spring MVC Application’ tutorial.

I will write a similar tutorial which describes how we can add social sign in to a Spring powered REST API in the future. In the meantime, you might want to read the other parts of this tutorial.

  • You can get the example application of this blog post from Github.

Petri Kainulainen

Petri is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button