/* Copyright 2015 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.maritimecloud.identityregistry.command.user; import java.util.Arrays; import java.util.List; import java.util.UUID; import net.maritimecloud.common.spring.ApplicationContextProvider; import net.maritimecloud.identityregistry.command.api.ChangeUserEmailAddress; import net.maritimecloud.identityregistry.command.api.ChangeUserPassword; import net.maritimecloud.identityregistry.command.api.RegisterUser; import net.maritimecloud.identityregistry.command.api.ResetPasswordKeyGenerated; import net.maritimecloud.identityregistry.command.api.SendResetPasswordInstructions; import net.maritimecloud.identityregistry.command.api.UnconfirmedUserEmailAddressSupplied; import net.maritimecloud.identityregistry.command.api.UserAccountActivated; import net.maritimecloud.identityregistry.command.api.UserEmailAddressVerified; import net.maritimecloud.identityregistry.command.api.UserPasswordChanged; import net.maritimecloud.identityregistry.command.api.UserRegistered; import net.maritimecloud.identityregistry.command.api.VerifyEmailAddress; import net.maritimecloud.identityregistry.query.internal.InternalUserEntry; import net.maritimecloud.identityregistry.query.internal.InternalUserQueryRepository; import net.maritimecloud.identityregistry.domain.DomainRegistry; import net.maritimecloud.portal.infrastructure.service.SHA512EncryptionService; import org.axonframework.domain.Message; import org.axonframework.test.FixtureConfiguration; import org.axonframework.test.Fixtures; import org.axonframework.test.matchers.Matchers; import org.hamcrest.Description; import org.hamcrest.Factory; import org.hamcrest.Matcher; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.allOf; import org.hamcrest.TypeSafeMatcher; import org.junit.Before; import org.junit.Test; import static org.mockito.Matchers.any; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.context.ApplicationContext; /** * * @author Christoffer Børrild */ public class UserTest { private FixtureConfiguration<User> fixture; private static final String A_USERNAME = "A_USERNAME"; private static final String A_TOO_SHORT_USERNAME = "UN"; private static final String A_PASSWORD = "A_PASSWORD"; private static final String A_PASSWORD_OBFUSCATED = "$shiro1$SHA-512$1000$7OBI6Fc/9eYGe0CpprAjJA==$8SO4YOxn8NozJ8H4DOFpjZPUDTnp3JqbnVHIC5h2ya7x2LPB0exWsTQ0Rb0HOrxWhsFqf6M1+S/bQZ70FPlm4g=="; private static final String A_CHANGED_PASSWORD = "A_CHANGED_PASSWORD"; private static final String A_CHANGED_PASSWORD_OBFUSCATED = "$shiro1$SHA-512$1000$BnBwaRWsOVgucFfAqmXVuQ==$sFL4P1rf3n6XrL9VSRvJ5D3T7X+254jeIf4KfNSxslnqy1Rw3D5va52EMViYMX4rK1fxi0nBlWV6LDsEmQezWw=="; private static final String AN_EMAIL_ADDRESS = "A@VALID.EMAIL"; private static final String ANOTHER_EMAIL_ADDRESS = "ANOTHER@VALID.EMAIL"; private static final String AN_INVALID_EMAIL = "AN_INVALID_EMAIL"; private static final String AN_EMAIL_ADDRESS_VERIFICATION_CODE = UUID.randomUUID().toString(); private static final String ANOTHER_EMAIL_ADDRESS_VERIFICATION_CODE = UUID.randomUUID().toString(); private static final UserId aUserId = new UserId(UUID.randomUUID().toString()); @Mock InternalUserQueryRepository internalUserQueryRepository; @Mock ApplicationContext applicationContext; @Before public void setUp() throws Exception { fixture = Fixtures.newGivenWhenThenFixture(User.class); MockitoAnnotations.initMocks(this); Mockito.when(applicationContext.getBean("encryptionService")).thenReturn(new SHA512EncryptionService()); new ApplicationContextProvider().setApplicationContext(applicationContext); } @Test public void registerUser() { fixture.givenNoPriorActivity() .when(new RegisterUser(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD)) .expectEventsMatching( aSequenceOf( // note: pass in plain text password here - the matcher needs the original in order to tell of things went well! anEventLike(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) ) ); } @Test public void verifyEmailAddressShouldActivateUserAccountOnFirstOccurence() { fixture.given(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .when(new VerifyEmailAddress(aUserId, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .expectEvents( new UserEmailAddressVerified(aUserId, A_USERNAME, AN_EMAIL_ADDRESS), new UserAccountActivated(aUserId, A_USERNAME) ); } @Test public void verifyEmailAddressShouldBeIgnoredIfRepeated() { fixture.given( new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE), new UserEmailAddressVerified(aUserId, A_USERNAME, AN_EMAIL_ADDRESS) ) .when(new VerifyEmailAddress(aUserId, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .expectEvents(); } @Test public void changeUserEmailAddress() { fixture.given( new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE), new UserEmailAddressVerified(aUserId, A_USERNAME, AN_EMAIL_ADDRESS) ) .when(new ChangeUserEmailAddress(aUserId, ANOTHER_EMAIL_ADDRESS)) .expectEventsMatching(aSequenceOf( // note: we pass in an existing verification code just to make sure it is not reused (see matcher) // ... we expect a new randomly created code anEventLike(new UnconfirmedUserEmailAddressSupplied(aUserId, A_USERNAME, ANOTHER_EMAIL_ADDRESS, AN_EMAIL_ADDRESS_VERIFICATION_CODE))) ); } @Test public void verifyEmailAddressShouldFailOnInvalidCode() { fixture.given(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .when(new VerifyEmailAddress(aUserId, ANOTHER_EMAIL_ADDRESS_VERIFICATION_CODE)) .expectException(IllegalArgumentException.class); } @Test public void registerUserShouldFailOnInvalidEmail() { fixture.givenNoPriorActivity() .when(new RegisterUser(aUserId, A_USERNAME, AN_INVALID_EMAIL, A_PASSWORD)) .expectException(IllegalArgumentException.class); } @Test public void registerUserShouldFailOnTooShortUsername() { fixture.givenNoPriorActivity() .when(new RegisterUser(aUserId, A_TOO_SHORT_USERNAME, AN_INVALID_EMAIL, A_PASSWORD)) .expectException(IllegalArgumentException.class); } @Test public void changePassword() { fixture.given(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .when(new ChangeUserPassword(aUserId, A_PASSWORD, A_CHANGED_PASSWORD)) //.expectEvents(new UserPasswordChanged(aUserId, A_USERNAME, A_CHANGED_PASSWORD_OBFUSCATED)); // we have to use a customized matcher since we cannot reproduce to the same obfuscated password .expectEventsMatching(aSequenceOf( anEventLike(new UserPasswordChanged(aUserId, A_USERNAME, A_CHANGED_PASSWORD))) ); } @Test public void changePasswordShouldFailWhenEqualToUsername() { fixture.given(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .when(new ChangeUserPassword(aUserId, A_PASSWORD, "A_USERNAME")) .expectException(IllegalArgumentException.class); } @Test public void changePasswordShouldFailWhenEqualToOldPassword() { fixture.given(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .when(new ChangeUserPassword(aUserId, A_PASSWORD, A_PASSWORD)) .expectException(IllegalArgumentException.class); } @Test public void changePasswordShouldFailWhenUsingWrongPassword() { fixture.given(new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE)) .when(new ChangeUserPassword(aUserId, A_PASSWORD + "THAT_IS_WRONG", A_CHANGED_PASSWORD)) .expectException(IllegalArgumentException.class); } @Test public void resetPassword() { InternalUserEntry aUserEntry = new InternalUserEntry(); aUserEntry.setUserId(aUserId.identifier()); aUserEntry.setUsername(A_USERNAME); aUserEntry.setEmailAddress(AN_EMAIL_ADDRESS); Mockito.when(internalUserQueryRepository.findByEmailAddressIgnoreCase(any())).thenReturn(aUserEntry); UserCommandHandler commandHandler = new UserCommandHandler(fixture.getRepository(), internalUserQueryRepository); fixture.registerAnnotatedCommandHandler(commandHandler); fixture.given( new UserRegistered(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, A_PASSWORD_OBFUSCATED, AN_EMAIL_ADDRESS_VERIFICATION_CODE), new UserEmailAddressVerified(aUserId, A_USERNAME, AN_EMAIL_ADDRESS) ) .when(new SendResetPasswordInstructions(AN_EMAIL_ADDRESS)) .expectEventsMatching( aSequenceOf( anEventLike(new ResetPasswordKeyGenerated(aUserId, A_USERNAME, AN_EMAIL_ADDRESS, "dummy reset code")) ) ); } // ------------------------------------------------------------------------ // HELPER MATCHERS // ------------------------------------------------------------------------ @Factory public static Matcher<List<? extends Message<?>>> aSequenceOf(Matcher<?>... matchers) { Matcher<?>[] terminatedListOfMatchers = Arrays.copyOf(matchers, matchers.length + 1); terminatedListOfMatchers[matchers.length] = Matchers.andNoMore(); return Matchers.payloadsMatching(Matchers.exactSequenceOf(terminatedListOfMatchers)); } @Factory public static <T> Matcher<ResetPasswordKeyGenerated> anEventLike(ResetPasswordKeyGenerated event) { return allOf( instanceOf(ResetPasswordKeyGenerated.class), hasProperty("userId", equalTo(event.getUserId())), hasProperty("username", equalTo(event.getUsername())), hasProperty("emailAddress", equalTo(event.getEmailAddress())), hasProperty("resetPasswordKey") ); } @Factory public static <T> Matcher<UserPasswordChanged> anEventLike(UserPasswordChanged sourceEvent) { return new IsUserPasswordChangedEventMatcher(sourceEvent); } @Factory public static <T> Matcher<UserRegistered> anEventLike(UserRegistered sourceEvent) { return new IsSameUserRegisteredEventMatcher(sourceEvent); } @Factory public static <T> Matcher<UnconfirmedUserEmailAddressSupplied> anEventLike(UnconfirmedUserEmailAddressSupplied sourceEvent) { return new IsSameUnconfirmedUserEmailAddressSuppliedEventMatcher(sourceEvent); } public static class IsSameUserRegisteredEventMatcher extends TypeSafeMatcher<UserRegistered> { private final UserRegistered sourceEvent; public IsSameUserRegisteredEventMatcher(UserRegistered sourceEvent) { this.sourceEvent = sourceEvent; } @Override public boolean matchesSafely(UserRegistered event) { return sourceEvent.getPrefferedUsername().equals(event.getPrefferedUsername()) && sourceEvent.getEmailAddress().equals(event.getEmailAddress()) && sourceEvent.getUserId().equals(event.getUserId()) // *NOTE* the password of the sourceEvent MUST be plaintext!!! && DomainRegistry.encryptionService().valuesMatch(sourceEvent.getObfuscatedPassword(), event.getObfuscatedPassword()); } @Override public void describeTo(Description description) { description.appendText("UserRegistered event is not the same"); } } public static class IsUserPasswordChangedEventMatcher extends TypeSafeMatcher<UserPasswordChanged> { private final UserPasswordChanged sourceEvent; public IsUserPasswordChangedEventMatcher(UserPasswordChanged sourceEvent) { this.sourceEvent = sourceEvent; } @Override public boolean matchesSafely(UserPasswordChanged event) { return sourceEvent.getUserId().equals(event.getUserId()) && sourceEvent.getUsername().equals(event.getUsername()) // *NOTE* the password of the sourceEvent MUST be plaintext!!! && DomainRegistry.encryptionService().valuesMatch(sourceEvent.getObfuscatedChangedPassword(), event.getObfuscatedChangedPassword()); } @Override public void describeTo(Description description) { description.appendText("UserPasswordChanged event is not the same"); } } public static class IsSameUnconfirmedUserEmailAddressSuppliedEventMatcher extends TypeSafeMatcher<UnconfirmedUserEmailAddressSupplied> { private final UnconfirmedUserEmailAddressSupplied sourceEvent; public IsSameUnconfirmedUserEmailAddressSuppliedEventMatcher(UnconfirmedUserEmailAddressSupplied sourceEvent) { this.sourceEvent = sourceEvent; } @Override public boolean matchesSafely(UnconfirmedUserEmailAddressSupplied event) { return sourceEvent.getUserId().equals(event.getUserId()) && sourceEvent.getUsername().equals(event.getUsername()) && sourceEvent.getUnconfirmedEmailAddress().equals(event.getUnconfirmedEmailAddress()) // *NOTE* silly test that it is not using a predictable verificationcode, like the one from a previous verification && !sourceEvent.getEmailVerificationCode().equals(event.getEmailVerificationCode()); } @Override public void describeTo(Description description) { description.appendText("UnconfirmedUserEmailAddressSupplied event is not the same"); } } }