diff --git a/build.gradle b/build.gradle index 8bbb1c283..7be34ea21 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ configure(allprojects) { ] sourceSets.test.resources.srcDirs = [ - "src/test/resources", + "src/test/resources", "src/test/java" ] @@ -148,7 +148,7 @@ configure(subprojects) { subproject -> archives javadocJar } - configurations { + configurations { springReleaseTestRuntime.extendsFrom testRuntime springSnapshotTestRuntime.extendsFrom testRuntime } @@ -195,6 +195,7 @@ project("spring-social-core") { description = "Foundational module containing the ServiceProvider Connect Framework and Service API invocation support." dependencies { compile("org.springframework:spring-jdbc:$springVersion", optional) + compile("org.springframework.data:spring-data-redis:$springDataRedisVersion", optional) compile("org.springframework:spring-web:$springVersion") compile("org.springframework.security:spring-security-crypto:$springSecurityVersion", optional) compile("org.apache.httpcomponents:httpclient:$httpComponentsVersion", optional) @@ -202,6 +203,8 @@ project("spring-social-core") { testCompile("org.springframework:spring-test:$springVersion") testCompile("javax.servlet:javax.servlet-api:$servletApiVersion", provided) testCompile("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") + testCompile("org.apache.commons:commons-pool2:$apacheCommonsPoolVersion") + testCompile("redis.clients:jedis:2.8.1") } } @@ -256,7 +259,7 @@ configure(rootProject) { dependencies { // for integration tests } - + task api(type: Javadoc) { group = "Documentation" description = "Generates aggregated Javadoc API documentation." @@ -310,7 +313,7 @@ artifacts { archives dist archives project(':docs').docsZip archives project(':docs').schemaZip -} +} task wrapper(type: Wrapper) { gradleVersion = "1.12" diff --git a/gradle.properties b/gradle.properties index 9c7f88c76..90f7998c8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,7 @@ h2Version=1.3.176 springSecurityVersion=3.2.9.RELEASE +springDataRedisVersion=1.7.1.RELEASE +apacheCommonsPoolVersion=2.2 junitVersion=4.12 httpComponentsVersion=4.3.6 aspectjVersion=1.8.5 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a6cc5b424..e1fc8a2be 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jun 30 12:43:52 CDT 2014 +#Fri May 27 21:26:51 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip diff --git a/spring-social-core/src/main/java/org/springframework/social/connect/redis/RedisConnectionRepository.java b/spring-social-core/src/main/java/org/springframework/social/connect/redis/RedisConnectionRepository.java new file mode 100644 index 000000000..6da91c16b --- /dev/null +++ b/spring-social-core/src/main/java/org/springframework/social/connect/redis/RedisConnectionRepository.java @@ -0,0 +1,183 @@ +package org.springframework.social.connect.redis; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.social.connect.*; +import org.springframework.social.connect.redis.data.SocialRedisConnection; +import org.springframework.social.connect.redis.data.SocialRedisConnectionRepository; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +public class RedisConnectionRepository implements ConnectionRepository { + + private final ConnectionFactoryLocator connectionFactoryLocator; + private final TextEncryptor textEncryptor; + private final SocialRedisConnectionRepository socialRedisConnectionRepository; + private final String userId; + + public RedisConnectionRepository(final ConnectionFactoryLocator connectionFactoryLocator, final TextEncryptor textEncryptor, final SocialRedisConnectionRepository socialRedisConnectionRepository, final String userId) { + Assert.notNull(socialRedisConnectionRepository, "socialRedisConnectionRepository is required"); + Assert.notNull(userId, "userId is required"); + + this.userId = userId; + this.socialRedisConnectionRepository = socialRedisConnectionRepository; + this.connectionFactoryLocator = connectionFactoryLocator; + this.textEncryptor = textEncryptor; + } + + public MultiValueMap> findAllConnections() { + Iterable allConnections = socialRedisConnectionRepository.findByUserId(userId); + + final MultiValueMap> connections = new LinkedMultiValueMap>(); + Set registeredProviderIds = connectionFactoryLocator.registeredProviderIds(); + for (String registeredProviderId : registeredProviderIds) { + connections.put(registeredProviderId, new ArrayList>()); + } + + for (SocialRedisConnection connection : allConnections) { + connections.add(connection.getProviderId(), connectionMapper.mapConnection(connection)); + } + + return connections; + } + + public List> findConnections(String providerId) { + Iterable connections = socialRedisConnectionRepository.findByProviderId(providerId); + + List> providerConnections = new ArrayList>(); + for (SocialRedisConnection connection : connections) { + providerConnections.add(connectionMapper.mapConnection(connection)); + } + + return providerConnections; + } + + public List> findConnections(Class apiType) { + List connections = findConnections(getProviderId(apiType)); + return (List>) connections; + } + + public MultiValueMap> findConnectionsToUsers(MultiValueMap providerUserIds) { + return null; + } + + public Connection getConnection(ConnectionKey connectionKey) { + try { + return connectionMapper.mapConnection(socialRedisConnectionRepository.findOneByUserIdAndProviderIdAndProviderUserId(userId, connectionKey.getProviderId(), connectionKey.getProviderUserId())); + } catch (EmptyResultDataAccessException e) { + throw new NoSuchConnectionException(connectionKey); + } + } + + public Connection getConnection(Class apiType, String providerUserId) { + String providerId = getProviderId(apiType); + return (Connection) getConnection(new ConnectionKey(providerId, providerUserId)); + } + + public Connection getPrimaryConnection(Class apiType) { + String providerId = getProviderId(apiType); + Connection connection = (Connection) findPrimaryConnection(providerId); + if (connection == null) { + throw new NotConnectedException(providerId); + } + return connection; + } + + public Connection findPrimaryConnection(Class apiType) { + String providerId = getProviderId(apiType); + return (Connection) findPrimaryConnection(providerId); + } + + public void addConnection(Connection connection) { + try { + ConnectionData data = connection.createData(); + SocialRedisConnection redisConnection = new SocialRedisConnection(data.getProviderUserId(), userId, data.getProviderId(), data.getDisplayName(), data.getProfileUrl(), data.getImageUrl(), encrypt(data.getAccessToken()), encrypt(data.getSecret()), encrypt(data.getRefreshToken()), data.getExpireTime()); + socialRedisConnectionRepository.save(redisConnection); + } catch (Exception e) { + throw new DuplicateConnectionException(connection.getKey()); + } + } + + public void updateConnection(Connection connection) { + ConnectionData data = connection.createData(); + SocialRedisConnection redisConnection = socialRedisConnectionRepository.findOneByUserIdAndProviderIdAndProviderUserId(userId, data.getProviderId(), data.getProviderUserId()); + + redisConnection.setDisplayName(data.getDisplayName()); + redisConnection.setImageUrl(data.getImageUrl()); + redisConnection.setProfileUrl(data.getProfileUrl()); + redisConnection.setAccessToken(encrypt(data.getAccessToken())); + redisConnection.setSecret(encrypt(data.getSecret())); + redisConnection.setRefreshToken(encrypt(data.getRefreshToken())); + redisConnection.setExpireTime(data.getExpireTime()); + + socialRedisConnectionRepository.save(redisConnection); + } + + public void removeConnections(String providerId) { + Iterable connections = socialRedisConnectionRepository.findByUserIdAndProviderId(userId, providerId); + + for (SocialRedisConnection redisConnection : connections) { + socialRedisConnectionRepository.delete(redisConnection); + } + } + + public void removeConnection(ConnectionKey connectionKey) { + // TODO: Wait for DATAKV-135 in order to use this: + // socialRedisConnectionRepository.deleteByUserIdAndProviderIdAndProviderUserId + // (userId, connectionKey.getProviderId(), connectionKey.getProviderUserId()); + + SocialRedisConnection socialRedisConnection = socialRedisConnectionRepository.findOneByUserIdAndProviderIdAndProviderUserId(userId, connectionKey.getProviderId(), connectionKey.getProviderUserId()); + socialRedisConnectionRepository.delete(socialRedisConnection); + } + + private final RedisConnectionMapper connectionMapper = new RedisConnectionMapper(); + + private final class RedisConnectionMapper { + + Connection mapConnection(final SocialRedisConnection redisConnection) { + if (redisConnection == null) { + throw new EmptyResultDataAccessException(1); + } + ConnectionData connectionData = mapConnectionData(redisConnection); + ConnectionFactory connectionFactory = connectionFactoryLocator.getConnectionFactory(connectionData.getProviderId()); + return connectionFactory.createConnection(connectionData); + } + + private ConnectionData mapConnectionData(final SocialRedisConnection redisConnection) { + return new ConnectionData(redisConnection.getProviderId(), redisConnection.getProviderUserId(), redisConnection.getDisplayName(), redisConnection.getProfileUrl(), redisConnection.getImageUrl(), + decrypt(redisConnection.getAccessToken()), decrypt(redisConnection.getSecret()), decrypt(redisConnection.getRefreshToken()), redisConnection.getExpireTime()); + } + + private String decrypt(String encryptedText) { + return encryptedText == null ? null : textEncryptor.decrypt(encryptedText); + } + } + + private Connection findPrimaryConnection(String providerId) { + Iterable redisConnections = socialRedisConnectionRepository.findByUserIdAndProviderId(userId, providerId); + + List> primaryConnections = new ArrayList>(); + for (SocialRedisConnection connection : redisConnections) { + primaryConnections.add(connectionMapper.mapConnection(connection)); + } + + if (primaryConnections.size() > 0) { + return primaryConnections.get(0); + } else { + return null; + } + } + + private String getProviderId(Class apiType) { + return connectionFactoryLocator.getConnectionFactory(apiType).getProviderId(); + } + + private String encrypt(String text) { + return text == null ? null : textEncryptor.encrypt(text); + } +} diff --git a/spring-social-core/src/main/java/org/springframework/social/connect/redis/RedisUsersConnectionRepository.java b/spring-social-core/src/main/java/org/springframework/social/connect/redis/RedisUsersConnectionRepository.java new file mode 100644 index 000000000..dbae3d1ae --- /dev/null +++ b/spring-social-core/src/main/java/org/springframework/social/connect/redis/RedisUsersConnectionRepository.java @@ -0,0 +1,62 @@ +package org.springframework.social.connect.redis; + +import org.springframework.security.crypto.encrypt.TextEncryptor; +import org.springframework.social.connect.Connection; +import org.springframework.social.connect.ConnectionFactoryLocator; +import org.springframework.social.connect.ConnectionRepository; +import org.springframework.social.connect.UsersConnectionRepository; +import org.springframework.social.connect.redis.data.SocialRedisConnection; +import org.springframework.social.connect.redis.data.SocialRedisConnectionRepository; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class RedisUsersConnectionRepository implements UsersConnectionRepository { + + private final ConnectionFactoryLocator connectionFactoryLocator; + private final TextEncryptor textEncryptor; + private final SocialRedisConnectionRepository socialRedisConnectionRepository; + + public RedisUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator, TextEncryptor textEncryptor, SocialRedisConnectionRepository socialRedisConnectionRepository) { + Assert.notNull(connectionFactoryLocator); + Assert.notNull(textEncryptor); + Assert.notNull(socialRedisConnectionRepository); + + this.connectionFactoryLocator = connectionFactoryLocator; + this.textEncryptor = textEncryptor; + this.socialRedisConnectionRepository = socialRedisConnectionRepository; + } + + public List findUserIdsWithConnection(final Connection connection) { + String providerId = connection.getKey().getProviderId(); + String providerUserId = connection.getKey().getProviderUserId(); + + Iterable connections = socialRedisConnectionRepository.findByProviderIdAndProviderUserId(providerId, providerUserId); + + List userIds = new ArrayList(); + for (SocialRedisConnection socialRedisConnection : connections) { + userIds.add(socialRedisConnection.getUserId()); + } + + return userIds; + } + + public Set findUserIdsConnectedTo(final String providerId, final Set providerUserIds) { + Set userIds = new HashSet(); + + for (String providerUserId : providerUserIds) { + for (SocialRedisConnection socialRedisConnection : socialRedisConnectionRepository.findByProviderIdAndProviderUserId(providerId, providerUserId)) { + userIds.add(socialRedisConnection.getUserId()); + } + } + + return userIds; + } + + public ConnectionRepository createConnectionRepository(final String userId) { + return new RedisConnectionRepository(connectionFactoryLocator, textEncryptor, socialRedisConnectionRepository, userId); + } +} diff --git a/spring-social-core/src/main/java/org/springframework/social/connect/redis/data/SocialRedisConnection.java b/spring-social-core/src/main/java/org/springframework/social/connect/redis/data/SocialRedisConnection.java new file mode 100644 index 000000000..984c1c8a0 --- /dev/null +++ b/spring-social-core/src/main/java/org/springframework/social/connect/redis/data/SocialRedisConnection.java @@ -0,0 +1,126 @@ +package org.springframework.social.connect.redis.data; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash("socialRedisConnection") +public class SocialRedisConnection { + + @Id + @Indexed + private String providerUserId; + + @Indexed + private String userId; + + @Indexed + private String providerId; + + private String displayName; + + private String profileUrl; + + private String imageUrl; + + private String accessToken; + + private String secret; + + private String refreshToken; + + private Long expireTime; + + public SocialRedisConnection(String providerUserId, String userId, String providerId, String displayName, String profileUrl, String imageUrl, String accessToken, String secret, String refreshToken, Long expireTime) { + this.providerUserId = providerUserId; + this.userId = userId; + this.providerId = providerId; + this.displayName = displayName; + this.profileUrl = profileUrl; + this.imageUrl = imageUrl; + this.accessToken = accessToken; + this.secret = secret; + this.refreshToken = refreshToken; + this.expireTime = expireTime; + } + + public String getProviderUserId() { + return providerUserId; + } + + public String getUserId() { + return userId; + } + + public String getProviderId() { + return providerId; + } + + public String getDisplayName() { + return displayName; + } + + public String getProfileUrl() { + return profileUrl; + } + + public String getImageUrl() { + return imageUrl; + } + + public String getAccessToken() { + return accessToken; + } + + public String getSecret() { + return secret; + } + + public String getRefreshToken() { + return refreshToken; + } + + public Long getExpireTime() { + return expireTime; + } + + public void setProviderUserId(String providerUserId) { + this.providerUserId = providerUserId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public void setProviderId(String providerId) { + this.providerId = providerId; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public void setProfileUrl(String profileUrl) { + this.profileUrl = profileUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public void setExpireTime(Long expireTime) { + this.expireTime = expireTime; + } +} diff --git a/spring-social-core/src/main/java/org/springframework/social/connect/redis/data/SocialRedisConnectionRepository.java b/spring-social-core/src/main/java/org/springframework/social/connect/redis/data/SocialRedisConnectionRepository.java new file mode 100644 index 000000000..5ab0219ea --- /dev/null +++ b/spring-social-core/src/main/java/org/springframework/social/connect/redis/data/SocialRedisConnectionRepository.java @@ -0,0 +1,21 @@ +package org.springframework.social.connect.redis.data; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface SocialRedisConnectionRepository extends CrudRepository { + + Iterable findByProviderId(final String providerId); + + Iterable findByUserIdAndProviderId(final String userId, final String providerId); + + SocialRedisConnection findOneByUserIdAndProviderIdAndProviderUserId(String userId, String providerId, String providerUserId); + + // TODO: will be available once DATAKV-135 is finished + void deleteByUserIdAndProviderIdAndProviderUserId(final String userId, final String providerId, final String providerUserId); + + Iterable findByUserId(final String userId); + + Iterable findByProviderIdAndProviderUserId(final String providerId, final String providerUserId); +} diff --git a/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisAvailable.java b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisAvailable.java new file mode 100644 index 000000000..c1cc81cf3 --- /dev/null +++ b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisAvailable.java @@ -0,0 +1,12 @@ +package org.springframework.social.connect.redis; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@interface RedisAvailable { + +} \ No newline at end of file diff --git a/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisAvailableRule.java b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisAvailableRule.java new file mode 100644 index 000000000..4887c58f0 --- /dev/null +++ b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisAvailableRule.java @@ -0,0 +1,70 @@ +package org.springframework.social.connect.redis; + +import org.junit.Assume; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + +class RedisAvailableRule implements TestRule { + + private static ThreadLocal connectionFactoryResource = new ThreadLocal<>(); + + private String redisHost; + + private int redisPort; + + RedisAvailableRule(final String redisHost, final int redisPort) { + this.redisHost = redisHost; + this.redisPort = redisPort; + } + + @Override + public Statement apply(final Statement base, Description description) { + RedisAvailable redisAvailable = description.getAnnotation(RedisAvailable.class); + if (redisAvailable != null) { + JedisConnectionFactory connectionFactory = null; + try { + connectionFactory = new JedisConnectionFactory(); + connectionFactory.setHostName(redisHost); + connectionFactory.setPort(redisPort); + connectionFactory.afterPropertiesSet(); + connectionFactory.getConnection(); + connectionFactoryResource.set(connectionFactory); + } catch (Exception e) { + if (connectionFactory != null) { + connectionFactory.destroy(); + } + return new Statement() { + @Override + public void evaluate() throws Throwable { + Assume.assumeTrue("Skipping test class: Redis not available at port " + redisHost + ":" + redisPort, false); + } + }; + } + + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } finally { + JedisConnectionFactory connectionFactory = connectionFactoryResource.get(); + connectionFactoryResource.remove(); + if (connectionFactory != null) { + connectionFactory.destroy(); + } + } + } + }; + } + + return new Statement() { + @Override + public void evaluate() throws Throwable { + base.evaluate(); + } + }; + + } +} diff --git a/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisConnectionRepositoryIntegrationTests.java b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisConnectionRepositoryIntegrationTests.java new file mode 100644 index 000000000..d360dae50 --- /dev/null +++ b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisConnectionRepositoryIntegrationTests.java @@ -0,0 +1,161 @@ +package org.springframework.social.connect.redis; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.social.connect.*; +import org.springframework.social.connect.redis.data.SocialRedisConnectionRepository; +import org.springframework.social.connect.support.ConnectionFactoryRegistry; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.MultiValueMap; +import redis.clients.jedis.JedisShardInfo; + +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@RedisAvailable +public class RedisConnectionRepositoryIntegrationTests { + + private static final String REDIS_HOST = "127.0.0.1"; + private static final int REDIS_PORT = 6379; + + @ClassRule + public static TestRule redisAvailableRule = new RedisAvailableRule(REDIS_HOST, REDIS_PORT); + + @Autowired + @SuppressWarnings("SpringJavaAutowiringInspection") + private SocialRedisConnectionRepository socialRedisConnectionRepository; + + @Autowired + private ConnectionFactoryLocator connectionFactoryLocator; + + private RedisConnectionRepository redisConnectionRepository; + + private RedisUtils.TestConnection testConnection; + + @Before + public void init() { + redisConnectionRepository = new RedisConnectionRepository(connectionFactoryLocator, Encryptors.noOpText(), socialRedisConnectionRepository, "Turbots"); + testConnection = new RedisUtils.TestConnection(new RedisUtils.TestTwitterApiAdapter(), "twitter:dhubau"); + socialRedisConnectionRepository.deleteAll(); + } + + @Test + public void addConnection() { + redisConnectionRepository.addConnection(testConnection); + + MultiValueMap> map = redisConnectionRepository.findAllConnections(); + + assertEquals(1, map.get("twitter").size()); + } + + @Test + public void removeConnections() { + redisConnectionRepository.addConnection(testConnection); + + redisConnectionRepository.removeConnections("twitter"); + + MultiValueMap> map = redisConnectionRepository.findAllConnections(); + + assertEquals(0, map.get("twitter").size()); + } + + @Test + public void removeConnection() { + redisConnectionRepository.addConnection(testConnection); + + redisConnectionRepository.removeConnection(testConnection.getKey()); + + MultiValueMap> map = redisConnectionRepository.findAllConnections(); + + assertEquals(0, map.get("twitter").size()); + } + + @Test + public void getConnectionWhenFound() { + redisConnectionRepository.addConnection(testConnection); + + Connection socialRedisConnection = redisConnectionRepository.getConnection(testConnection.getKey()); + assertNotNull(socialRedisConnection); + assertEquals(testConnection.getKey().getProviderUserId(), socialRedisConnection.getKey().getProviderUserId()); + } + + @Test(expected = NoSuchConnectionException.class) + public void getConnectionWhenNotFound() { + ConnectionKey connectionKey = new ConnectionKey("foo", "bar"); + redisConnectionRepository.getConnection(connectionKey); + } + + @Test + public void updateConnection() { + assertNull(testConnection.getDisplayName()); + redisConnectionRepository.addConnection(testConnection); + + redisConnectionRepository.updateConnection(testConnection); + + Connection socialRedisConnection = redisConnectionRepository.getConnection(testConnection.getKey()); + assertNotNull(socialRedisConnection); + assertEquals("displayName", socialRedisConnection.getDisplayName()); + } + + @Test + public void getPrimaryConnectionOK() { + redisConnectionRepository.addConnection(testConnection); + + Connection connection = redisConnectionRepository.getPrimaryConnection(RedisUtils.TestTwitterApi.class); + + assertNotNull(connection); + assertEquals(connection.getDisplayName(), "displayName"); + } + + @Test(expected = NotConnectedException.class) + public void getPrimaryConnectionException() throws NotConnectedException { + redisConnectionRepository.getPrimaryConnection(RedisUtils.TestTwitterApi.class); + } + + @Test + public void findPrimaryConnection() { + redisConnectionRepository.addConnection(testConnection); + + Connection connection = redisConnectionRepository.findPrimaryConnection(RedisUtils.TestTwitterApi.class); + + assertNotNull(connection); + assertEquals(connection.getDisplayName(), "displayName"); + } + + @Configuration + @EnableRedisRepositories(includeFilters = {@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*SocialRedisConnectionRepository")}) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(new JedisShardInfo(REDIS_HOST, REDIS_PORT)); + connectionFactory.afterPropertiesSet(); + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + return template; + } + + @Bean + public ConnectionFactoryLocator connectionFactoryLocator() { + ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry(); + registry.addConnectionFactory(new RedisUtils.TestTwitterConnectionFactory()); + return registry; + } + } +} diff --git a/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisUsersConnectionRepositoryIntegrationTests.java b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisUsersConnectionRepositoryIntegrationTests.java new file mode 100644 index 000000000..6c2fa73d4 --- /dev/null +++ b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisUsersConnectionRepositoryIntegrationTests.java @@ -0,0 +1,117 @@ +package org.springframework.social.connect.redis; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.social.connect.ConnectionFactoryLocator; +import org.springframework.social.connect.ConnectionRepository; +import org.springframework.social.connect.redis.data.SocialRedisConnectionRepository; +import org.springframework.social.connect.support.ConnectionFactoryRegistry; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import redis.clients.jedis.JedisShardInfo; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@RedisAvailable +public class RedisUsersConnectionRepositoryIntegrationTests { + + private static final String REDIS_HOST = "127.0.0.1"; + private static final int REDIS_PORT = 6379; + + @Autowired + @SuppressWarnings("SpringJavaAutowiringInspection") + private SocialRedisConnectionRepository socialRedisConnectionRepository; + + @ClassRule + public static TestRule redisAvailableRule = new RedisAvailableRule(REDIS_HOST, REDIS_PORT); + + @Autowired + private ConnectionFactoryLocator connectionFactoryLocator; + + private RedisUsersConnectionRepository redisUsersConnectionRepository; + + private RedisUtils.TestConnection connectionForDhubau; + private RedisUtils.TestConnection connectionForMyBuddy; + + @Before + public void init() { + redisUsersConnectionRepository = new RedisUsersConnectionRepository(connectionFactoryLocator, Encryptors.noOpText(), socialRedisConnectionRepository); + connectionForDhubau = new RedisUtils.TestConnection(new RedisUtils.TestTwitterApiAdapter(), "twitter:dhubau"); + connectionForMyBuddy = new RedisUtils.TestConnection(new RedisUtils.TestTwitterApiAdapter(), "twitter:buddy"); + socialRedisConnectionRepository.deleteAll(); + } + + @Test + public void findUserIdsWithConnectionNoUsers() { + List userIds = redisUsersConnectionRepository.findUserIdsWithConnection(connectionForDhubau); + + assertEquals(0, userIds.size()); + } + + @Test + public void findUserIdsWithConnectionOneUser() { + ConnectionRepository connectionRepository = redisUsersConnectionRepository.createConnectionRepository("dhubau"); + connectionRepository.addConnection(connectionForDhubau); + connectionRepository.addConnection(connectionForMyBuddy); + + List userIds = redisUsersConnectionRepository.findUserIdsWithConnection(connectionForDhubau); + + assertEquals(1, userIds.size()); + } + + @Test + public void findUserIdsConnectedToWithTwoUsers() { + ConnectionRepository connectionRepository = redisUsersConnectionRepository.createConnectionRepository("dhubau"); + connectionRepository.addConnection(connectionForDhubau); + connectionRepository = redisUsersConnectionRepository.createConnectionRepository("buddy"); + connectionRepository.addConnection(connectionForMyBuddy); + + Set providerUserIds = new HashSet<>(); + Collections.addAll(providerUserIds, "twitter:dhubau", "twitter:buddy"); + Set userIds = redisUsersConnectionRepository.findUserIdsConnectedTo("twitter", providerUserIds); + + assertEquals(2, userIds.size()); + } + + @Configuration + @EnableRedisRepositories(includeFilters = {@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*SocialRedisConnectionRepository")}) + static class Config { + + @Bean + RedisTemplate redisTemplate() { + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(new JedisShardInfo(REDIS_HOST, REDIS_PORT)); + connectionFactory.afterPropertiesSet(); + + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + return template; + } + + @Bean + public ConnectionFactoryLocator connectionFactoryLocator() { + ConnectionFactoryRegistry registry = new ConnectionFactoryRegistry(); + registry.addConnectionFactory(new RedisUtils.TestTwitterConnectionFactory()); + return registry; + } + } +} diff --git a/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisUtils.java b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisUtils.java new file mode 100644 index 000000000..95e70eec9 --- /dev/null +++ b/spring-social-core/src/test/java/org/springframework/social/connect/redis/RedisUtils.java @@ -0,0 +1,91 @@ +package org.springframework.social.connect.redis; + +import org.springframework.social.connect.*; +import org.springframework.social.connect.support.AbstractConnection; +import org.springframework.social.connect.support.OAuth1ConnectionFactory; +import org.springframework.social.oauth1.OAuth1Operations; +import org.springframework.social.oauth1.OAuth1ServiceProvider; + +class RedisUtils { + + static class TestConnection extends AbstractConnection { + + private String providerUserId; + + TestConnection(final ApiAdapter apiAdapter, final String providerUserId) { + super(apiAdapter); + this.providerUserId = providerUserId; + } + + @Override + public Object getApi() { + return new TestTwitterApi() { + @Override + public String getAccessToken() { + return "accessToken-123"; + } + }; + } + + @Override + public ConnectionKey getKey() { + return new ConnectionKey("twitter", providerUserId); + } + + @Override + public ConnectionData createData() { + return new ConnectionData("twitter", providerUserId, "displayName", "profileUrl", "imageUrl", "accessToken", "secret", "refreshToken", 1000000L); + } + } + + static class TestTwitterConnectionFactory extends OAuth1ConnectionFactory { + + TestTwitterConnectionFactory() { + super("twitter", new TestTwitterServiceProvider(), new TestTwitterApiAdapter()); + } + + } + + private static class TestTwitterServiceProvider implements OAuth1ServiceProvider { + + public OAuth1Operations getOAuthOperations() { + return null; + } + + public TestTwitterApi getApi(final String accessToken, final String secret) { + return new TestTwitterApi() { + public String getAccessToken() { + return accessToken; + } + + }; + } + + } + + interface TestTwitterApi { + + String getAccessToken(); + + } + + static class TestTwitterApiAdapter implements ApiAdapter { + + private String name = "@dhubau"; + + public boolean test(TestTwitterApi api) { + return true; + } + + public void setConnectionValues(TestTwitterApi api, ConnectionValues values) { + } + + public UserProfile fetchUserProfile(TestTwitterApi api) { + return new UserProfileBuilder().setName(name).setUsername(name).build(); + } + + public void updateStatus(TestTwitterApi api, String message) { + } + + } +}