package io.kaif.service.impl;
import static org.junit.Assert.*;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.EnumSet;
import java.util.Locale;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import io.kaif.model.account.Account;
import io.kaif.model.account.AccountAccessToken;
import io.kaif.model.account.AccountAuth;
import io.kaif.model.account.AccountDao;
import io.kaif.model.account.AccountOnceToken;
import io.kaif.model.account.AccountSecret;
import io.kaif.model.account.AccountStats;
import io.kaif.model.account.Authority;
import io.kaif.model.account.Authorization;
import io.kaif.model.exception.OldPasswordNotMatchException;
import io.kaif.model.exception.RequireCitizenException;
import io.kaif.test.DbIntegrationTests;
import io.kaif.web.support.AccessDeniedException;
public class AccountServiceImplTest extends DbIntegrationTests {
@Autowired
private AccountServiceImpl service;
@Autowired
private AccountSecret accountSecret;
@Autowired
private AccountDao accountDao;
private Locale lc = Locale.TAIWAN;
@Test
public void createViaEmail() {
Account account = service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
Account loaded = accountDao.findById(account.getAccountId()).get();
assertEquals(account, loaded);
assertEquals("foo@gmail.com", loaded.getEmail());
assertFalse(loaded.isActivated());
assertEquals(EnumSet.of(Authority.TOURIST), loaded.getAuthorities());
verify(mockMailAgent).sendAccountActivation(eq(lc),
eq(account),
Mockito.matches("[a-z\\-0-9]{36}"));
AccountOnceToken token = accountDao.listOnceTokens().get(0);
assertEquals(account.getAccountId(), token.getAccountId());
assertEquals(AccountOnceToken.Type.ACTIVATION, token.getTokenType());
assertFalse(token.isExpired(Instant.now().plus(Duration.ofHours(23))));
assertFalse(token.isComplete());
AccountStats stats = service.loadAccountStats(account.getUsername());
assertEquals(AccountStats.zero(account.getAccountId()), stats);
}
@Test
public void resendActivation() {
Account account = service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
Mockito.reset(mockMailAgent);
service.resendActivation(account, lc);
verify(mockMailAgent).sendAccountActivation(eq(lc),
eq(account),
Mockito.matches("[a-z\\-0-9]{36}"));
assertEquals(2, accountDao.listOnceTokens().size());
}
@Test
public void findValidResetPasswordToken() throws Exception {
service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
service.sendResetPassword("myname", "foo@gmail.com", lc);
AccountOnceToken resetToken = accountDao.listOnceTokens().get(1);
assertTrue(service.findValidResetPasswordToken(resetToken.getToken()).isPresent());
accountDao.completeOnceToken(resetToken);
assertFalse(service.findValidResetPasswordToken(resetToken.getToken()).isPresent());
}
@Test
public void updatePasswordWithOnceToken() throws Exception {
Account account = service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
service.sendResetPassword("myname", "foo@gmail.com", lc);
AccountOnceToken resetToken = accountDao.listOnceTokens().get(1);
Mockito.reset(mockMailAgent);
service.updatePasswordWithOnceToken(resetToken.getToken(), "pwd456", lc);
verify(mockMailAgent).sendPasswordWasReset(eq(lc), eq(account));
assertFalse(service.findValidResetPasswordToken(resetToken.getToken()).isPresent());
assertTrue(service.authenticate("myname", "pwd456").isPresent());
//update again takes no effect
service.updatePasswordWithOnceToken(resetToken.getToken(), "pw ignored", lc);
assertFalse(service.authenticate("myname", "pw ignored").isPresent());
verifyNoMoreInteractions(mockMailAgent);
}
@Test
public void sendResetPassword_noSuchAccount() {
//case 1: no account
service.sendResetPassword("myname", "foo@gmail.com", lc);
verifyZeroInteractions(mockMailAgent);
assertEquals(0, accountDao.listOnceTokens().size());
}
@Test
public void sendResetPassword_emailNotMatch() {
service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
Mockito.reset(mockMailAgent);
service.sendResetPassword("myname", "wrong@gmail.com", lc);
verifyZeroInteractions(mockMailAgent);
assertEquals(1, accountDao.listOnceTokens().size());
}
@Test
public void sendResetPassword() {
Account account = service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
Mockito.reset(mockMailAgent);
service.sendResetPassword("myname", "foo@gmail.com", lc);
verify(mockMailAgent).sendResetPassword(eq(lc),
eq(account),
Mockito.matches("[a-z\\-0-9]{36}"));
assertTrue(accountDao.listOnceTokens()
.stream()
.anyMatch(token -> token.getTokenType() == AccountOnceToken.Type.FORGET_PASSWORD));
}
@Test
public void activate() throws Exception {
Account account = service.createViaEmail("xyz", "xyz@gmail.com", "595959", lc);
AccountOnceToken token = accountDao.listOnceTokens().get(0);
assertTrue(service.activate(token.getToken()));
Account loaded = accountDao.findById(account.getAccountId()).get();
assertTrue(loaded.isActivated());
assertTrue(loaded.getAuthorities().contains(Authority.CITIZEN));
assertFalse("activate twice should invalid", service.activate(token.getToken()));
assertFalse("not exist token should invalid", service.activate("not exist"));
}
@Test
public void activate_skip_token_expired() throws Exception {
service.setClock(Clock.offset(Clock.systemDefaultZone(), Duration.ofDays(-2)));
Account account = service.createViaEmail("xyz", "xyz@gmail.com", "595959", lc);
AccountOnceToken token = accountDao.listOnceTokens().get(0);
service.setClock(Clock.systemDefaultZone());
assertFalse("expired token should invalid", service.activate(token.getToken()));
Account loaded = accountDao.findById(account.getAccountId()).get();
assertFalse(loaded.isActivated());
}
@Test
public void oauthDirectAuthorizeToken() throws Exception {
Account account = savedAccountCitizen("oauthTest");
AccountOnceToken token = service.createOauthDirectAuthorizeToken(account);
assertTrue(service.oauthDirectAuthorize(token.getToken()).isPresent());
assertFalse("consumed token should be invalid",
service.oauthDirectAuthorize(token.getToken()).isPresent());
}
@Test
public void oauthDirectAuthorizeToken_skip_expired() throws Exception {
Account account = savedAccountCitizen("oauthTest");
service.setClock(Clock.offset(Clock.systemDefaultZone(), Duration.ofHours(-2)));
AccountOnceToken token = service.createOauthDirectAuthorizeToken(account);
service.setClock(Clock.systemDefaultZone());
assertFalse("expired token should be invalid",
service.oauthDirectAuthorize(token.getToken()).isPresent());
}
@Test
public void oauthDirectAuthorizeToken_requireCitizen() throws Exception {
Account tourist = savedAccountTourist("badOauthTest");
try {
service.createOauthDirectAuthorizeToken(tourist);
fail("RequireCitizenException expected");
} catch (RequireCitizenException expected) {
}
}
@Test
public void isNameAvailable() throws Exception {
assertTrue(service.isUsernameAvailable("xyz123"));
service.createViaEmail("xyz123", "foobar@gmail.com", "9999123", lc);
assertFalse(service.isUsernameAvailable("xyz123"));
assertFalse(service.isUsernameAvailable("XYZ123"));
}
@Test
public void isEmailAvailable() throws Exception {
assertTrue(service.isEmailAvailable("xyz123@foo.com"));
service.createViaEmail("xyz123", "xyz123@foo.com", "9999123", lc);
assertFalse(service.isEmailAvailable("xyz123@foo.com"));
assertFalse(service.isEmailAvailable("XYZ123@Foo.com"));
}
@Test
public void authenticate_case_insensitive() {
Instant now = Instant.now();
service.setClock(Clock.fixed(now, ZoneOffset.UTC));
service.createViaEmail("myName", "foo@gmail.com", "pwd123", lc);
AccountAuth auth = service.authenticate("myname", "pwd123").get();
assertEquals("myName", auth.getUsername());
}
@Test
public void authenticate() {
Instant now = Instant.now();
service.setClock(Clock.fixed(now, ZoneOffset.UTC));
assertFalse(service.authenticate("notexist", "pwd123").isPresent());
service.createViaEmail("myname", "foo@gmail.com", "pwd123", lc);
AccountAuth auth = service.authenticate("myName", "pwd123").get();
assertEquals("myname", auth.getUsername());
assertTrue(AccountAccessToken.tryDecode(auth.getAccessToken(), accountSecret).isPresent());
assertTrue(Instant.ofEpochMilli(auth.getExpireTime()).isAfter(now.plus(Duration.ofDays(7))));
assertEquals(now.toEpochMilli(), auth.getGenerateTime());
//failed case
assertFalse(service.authenticate("myname", "wrong pass").isPresent());
}
@Test
public void strongVerifyAccessToken() throws Exception {
Account account = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
AccountAuth accountAuth = service.authenticate("abc99", "pppwww").get();
assertTrue(service.strongVerifyAccessToken(accountAuth.getAccessToken()).isPresent());
//invalid case 1 bad token
assertFalse(service.strongVerifyAccessToken("badtoken").isPresent());
//invalid case 2, password changed
service.updateNewPassword(account, "pppwww", "newPw123", lc);
assertFalse(service.strongVerifyAccessToken(accountAuth.getAccessToken()).isPresent());
//invalid case 3, authorities changed
accountAuth = service.authenticate("abc99", "newPw123").get();
//use dao to update directly
accountDao.updateAuthorities(account, EnumSet.of(Authority.SUFFRAGE));
assertFalse(service.strongVerifyAccessToken(accountAuth.getAccessToken()).isPresent());
}
@Test
public void updateAuthorities() throws Exception {
Authorization account = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
EnumSet<Authority> set = EnumSet.of(Authority.CITIZEN, Authority.SYSOP);
service.updateAuthorities(account, set);
assertEquals(set, accountDao.findById(account.authenticatedId()).get().getAuthorities());
}
@Test
public void updateAuthorities_should_not_include_forbidden() throws Exception {
Authorization authorization = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
EnumSet<Authority> set = EnumSet.of(Authority.FORBIDDEN, Authority.SYSOP);
try {
service.updateAuthorities(authorization, set);
fail("IllegalArgumentException expected");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void updateNewPassword() throws Exception {
Account account = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
Mockito.reset(mockMailAgent);
AccountAuth accountAuth = service.updateNewPassword(account, "pppwww", "123456", lc);
verify(mockMailAgent).sendPasswordWasReset(eq(lc), eq(account));
assertNotNull(accountAuth);
assertTrue(service.authenticate("abc99", "123456").isPresent());
}
@Test(expected = OldPasswordNotMatchException.class)
public void updateNewPassword_oldPasswordNotMatch() throws Exception {
Account account = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
service.updateNewPassword(account, "wrong old pw", "123456", lc);
}
@Test
public void updateDescription() throws Exception {
Account account = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
assertEquals("", account.getRenderDescription());
assertEquals("<p>Hi I am a <em>developer</em></p>\n",
service.updateDescription(account, "Hi I am a *developer*"));
}
@Test
public void loadEditableDescription() throws Exception {
Account account = service.createViaEmail("abc99", "bar@gmail.com", "pppwww", lc);
assertEquals("", service.loadEditableDescription(account));
service.updateDescription(account, "Hi I am a <*developer*>");
assertEquals("Hi I am a <*developer*>", service.loadEditableDescription(account));
}
@Test
public void extendsAccessToken() throws Exception {
service.createViaEmail("bbbb99", "bar@gmail.com", "pppwww", lc);
AccountAuth accountAuth = service.authenticate("bbbb99", "pppwww").get();
AccountAccessToken accountAccessToken = service.strongVerifyAccessToken(accountAuth.getAccessToken())
.get();
AccountAuth extend = service.extendsAccessToken(accountAccessToken);
assertFalse(extend.equals(accountAuth));
assertTrue(service.strongVerifyAccessToken(extend.getAccessToken()).isPresent());
}
@Test
public void extendsAccessToken_verify_db_failed() throws Exception {
Account account = service.createViaEmail("bbbb99", "bar@gmail.com", "pppwww", lc);
AccountAuth accountAuth = service.authenticate("bbbb99", "pppwww").get();
AccountAccessToken outOfDateToken = service.strongVerifyAccessToken(accountAuth.getAccessToken())
.get();
service.updateNewPassword(account, "pppwww", "pw2newone", Locale.ENGLISH);
try {
service.extendsAccessToken(outOfDateToken);
fail("AccessDeniedException expected");
} catch (AccessDeniedException expected) {
}
}
@Test
public void loadAccount() {
Account account = service.createViaEmail("BBbb99", "bar@gmail.com", "pppwww", lc);
Account loaded = service.loadAccount("BBbb99");
assertEquals(account, loaded);
assertEquals("bar@gmail.com", loaded.getEmail());
assertFalse(loaded.isActivated());
assertEquals(EnumSet.of(Authority.TOURIST), loaded.getAuthorities());
loaded = service.loadAccount("Bbbb99");
assertEquals(account, loaded);
loaded = service.loadAccount("BbbB99");
assertEquals(account, loaded);
}
}