IdUserPasswordResetServiceTest.java
/*
* Copyright © 2023 Mark Raynsford <code@io7m.com> https://www.io7m.com
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package com.io7m.idstore.tests.server.controller.user_pwreset;
import com.io7m.idstore.database.api.IdDatabaseConnectionType;
import com.io7m.idstore.database.api.IdDatabaseException;
import com.io7m.idstore.database.api.IdDatabaseTransactionType;
import com.io7m.idstore.database.api.IdDatabaseType;
import com.io7m.idstore.database.api.IdDatabaseUsersQueriesType;
import com.io7m.idstore.model.IdEmail;
import com.io7m.idstore.model.IdName;
import com.io7m.idstore.model.IdNonEmptyList;
import com.io7m.idstore.model.IdPasswordAlgorithmRedacted;
import com.io7m.idstore.model.IdPasswordException;
import com.io7m.idstore.model.IdRealName;
import com.io7m.idstore.model.IdToken;
import com.io7m.idstore.model.IdUser;
import com.io7m.idstore.model.IdUserPasswordReset;
import com.io7m.idstore.server.api.IdServerConfiguration;
import com.io7m.idstore.server.api.IdServerConfigurations;
import com.io7m.idstore.server.controller.command_exec.IdCommandExecutionFailure;
import com.io7m.idstore.server.controller.user_pwreset.IdUserPasswordResetService;
import com.io7m.idstore.server.controller.user_pwreset.IdUserPasswordResetServiceType;
import com.io7m.idstore.server.service.branding.IdServerBrandingServiceType;
import com.io7m.idstore.server.service.clock.IdServerClock;
import com.io7m.idstore.server.service.configuration.IdServerConfigurationFiles;
import com.io7m.idstore.server.service.mail.IdServerMailServiceType;
import com.io7m.idstore.server.service.ratelimit.IdRateLimitPasswordResetServiceType;
import com.io7m.idstore.server.service.telemetry.api.IdEventServiceType;
import com.io7m.idstore.server.service.telemetry.api.IdServerTelemetryNoOp;
import com.io7m.idstore.server.service.telemetry.api.IdServerTelemetryServiceType;
import com.io7m.idstore.server.service.templating.IdFMEmailPasswordResetData;
import com.io7m.idstore.server.service.templating.IdFMTemplateServiceType;
import com.io7m.idstore.server.service.templating.IdFMTemplateType;
import com.io7m.idstore.strings.IdStrings;
import com.io7m.idstore.tests.IdFakeClock;
import com.io7m.idstore.tests.IdTestDirectories;
import com.io7m.idstore.tests.server.api.IdServerConfigurationsTest;
import com.io7m.idstore.tests.server.service.IdServiceContract;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.internal.verification.Times;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Clock;
import java.time.OffsetDateTime;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import static com.io7m.idstore.database.api.IdDatabaseRole.IDSTORE;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.HTTP_PARAMETER_INVALID;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.HTTP_PARAMETER_NONEXISTENT;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.IO_ERROR;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.MAIL_SYSTEM_FAILURE;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.PASSWORD_RESET_MISMATCH;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.PASSWORD_RESET_NONEXISTENT;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.RATE_LIMIT_EXCEEDED;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.SQL_ERROR;
import static com.io7m.idstore.error_codes.IdStandardErrorCodes.USER_NONEXISTENT;
import static java.util.Optional.empty;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public final class IdUserPasswordResetServiceTest
extends IdServiceContract<IdUserPasswordResetServiceType>
{
private static final IdUser FAKE_USER;
static {
try {
FAKE_USER = new IdUser(
UUID.randomUUID(),
new IdName("person"),
new IdRealName("A Real Name"),
IdNonEmptyList.single(new IdEmail("someone@example.com")),
OffsetDateTime.now(),
OffsetDateTime.now(),
IdPasswordAlgorithmRedacted.create().createHashed("a")
);
} catch (final IdPasswordException e) {
throw new RuntimeException(e);
}
}
private IdDatabaseType database;
private IdFMTemplateServiceType templating;
private IdFMTemplateType<IdFMEmailPasswordResetData> emailTemplate;
private IdFakeClock clock;
private IdRateLimitPasswordResetServiceType rateLimit;
private IdServerBrandingServiceType branding;
private IdServerClock serverClock;
private IdServerConfiguration configuration;
private IdServerMailServiceType mailService;
private IdStrings strings;
private IdServerTelemetryServiceType telemetry;
private Path directory;
private IdEventServiceType events;
@BeforeEach
public void setup()
throws Exception
{
this.directory =
IdTestDirectories.createTempDirectory();
final var file =
IdTestDirectories.resourceOf(
IdServerConfigurationsTest.class,
this.directory,
"server-config-0.xml"
);
final var configFile =
new IdServerConfigurationFiles()
.parse(file);
this.configuration =
IdServerConfigurations.ofFile(
Locale.getDefault(),
Clock.systemUTC(),
configFile
);
this.telemetry =
IdServerTelemetryNoOp.noop();
this.branding =
Mockito.mock(IdServerBrandingServiceType.class);
this.templating =
Mockito.mock(IdFMTemplateServiceType.class);
this.mailService =
Mockito.mock(IdServerMailServiceType.class);
this.clock =
new IdFakeClock();
this.serverClock =
new IdServerClock(this.clock);
this.database =
Mockito.mock(IdDatabaseType.class);
this.strings =
IdStrings.create(Locale.ROOT);
this.rateLimit =
Mockito.mock(IdRateLimitPasswordResetServiceType.class);
this.events =
Mockito.mock(IdEventServiceType.class);
this.emailTemplate =
Mockito.mock(IdFMTemplateType.class);
Mockito.when(this.templating.emailPasswordResetTemplate())
.thenReturn(this.emailTemplate);
Mockito.when(this.branding.title())
.thenReturn("idstore");
}
@AfterEach
public void tearDown()
throws IOException
{
IdTestDirectories.deleteDirectory(this.directory);
}
@Override
protected IdUserPasswordResetServiceType createInstanceA()
{
return IdUserPasswordResetService.create(
this.telemetry,
this.branding,
this.templating,
this.mailService,
this.configuration,
this.serverClock,
this.database,
this.strings,
this.rateLimit,
this.events
);
}
@Override
protected IdUserPasswordResetServiceType createInstanceB()
{
return IdUserPasswordResetService.create(
this.telemetry,
this.branding,
this.templating,
this.mailService,
this.configuration,
this.serverClock,
this.database,
this.strings,
this.rateLimit,
this.events
);
}
/**
* An email or username must be provided.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginNoUsernameOrEmail()
throws Exception
{
final var resets = this.createInstanceA();
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.empty()
);
});
assertEquals(HTTP_PARAMETER_NONEXISTENT, ex.errorCode());
}
/**
* Rate limiting prevents frequent resets.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginRateLimited()
throws Exception
{
final var resets = this.createInstanceA();
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.FALSE);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("someone@example.com"),
Optional.empty()
);
});
assertEquals(RATE_LIMIT_EXCEEDED, ex.errorCode());
}
/**
* A valid username must be provided.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginUsernameInvalid()
throws Exception
{
final var resets = this.createInstanceA();
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.of("not valid in the slightest $")
);
});
assertEquals(HTTP_PARAMETER_INVALID, ex.errorCode());
}
/**
* An existing username must be provided.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginUsernameNonexistent()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.of("nonexistent")
);
});
assertEquals(USER_NONEXISTENT, ex.errorCode());
}
/**
* An existing email must be provided.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginEmailNonexistent()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("nonexistent@example.com"),
Optional.empty()
);
});
assertEquals(USER_NONEXISTENT, ex.errorCode());
}
/**
* An email is sent if a username exists and everything else succeeds.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginUsernameOK()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
Mockito.when(users.userGetForName(new IdName("person")))
.thenReturn(Optional.of(FAKE_USER));
Mockito.when(this.mailService.sendMail(
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any()
)).thenReturn(CompletableFuture.completedFuture(null));
final var requestId = UUID.randomUUID();
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
requestId,
Optional.empty(),
Optional.of("person")
);
Mockito.verify(this.mailService, new Times(1))
.sendMail(
Mockito.any(),
Mockito.eq(requestId),
Mockito.eq(FAKE_USER.emails().first()),
Mockito.any(),
Mockito.any(),
Mockito.any()
);
}
/**
* An email is sent if an email exists and everything else succeeds.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginEmailOK()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
Mockito.when(users.userGetForEmail(FAKE_USER.emails().first()))
.thenReturn(Optional.of(FAKE_USER));
Mockito.when(this.mailService.sendMail(
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any()
)).thenReturn(CompletableFuture.completedFuture(null));
final var requestId = UUID.randomUUID();
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
requestId,
Optional.of(FAKE_USER.emails().first().value()),
Optional.empty()
);
Mockito.verify(this.mailService, new Times(1))
.sendMail(
Mockito.any(),
Mockito.eq(requestId),
Mockito.eq(FAKE_USER.emails().first()),
Mockito.any(),
Mockito.any(),
Mockito.any()
);
}
/**
* If the mail system fails, the operation fails.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginMailSystemFails()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
Mockito.when(users.userGetForName(new IdName("person")))
.thenReturn(Optional.of(FAKE_USER));
final var exception =
new IOException("Something failed.");
Mockito.when(this.mailService.sendMail(
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any(),
Mockito.any()
)).thenReturn(CompletableFuture.failedFuture(exception));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.of("person")
);
});
assertEquals(MAIL_SYSTEM_FAILURE, ex.errorCode());
assertEquals(exception, ex.getCause().getCause());
}
/**
* If the template fails, the operation fails.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginTemplateFails()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
Mockito.when(users.userGetForName(new IdName("person")))
.thenReturn(Optional.of(FAKE_USER));
Mockito.doThrow(new IOException("Template failed."))
.when(this.emailTemplate)
.process(Mockito.any(), Mockito.any());
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.of("person")
);
});
assertEquals(IO_ERROR, ex.errorCode());
}
/**
* If the database fails, the operation fails.
*
* @throws Exception On errors
*/
@Test
public void testResetBeginDatabaseFails()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(this.rateLimit.isAllowedByRateLimit("127.0.0.1"))
.thenReturn(Boolean.TRUE);
Mockito.when(users.userGetForName(new IdName("person")))
.thenThrow(new IdDatabaseException("Ouch", SQL_ERROR, Map.of(), empty()));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetBegin(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.of("person")
);
});
assertEquals(SQL_ERROR, ex.errorCode());
}
/**
* If the correct token is provided, and the token has not expired, the check
* succeeds.
*
* @throws Exception On errors
*/
@Test
public void testCheckOK()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
final var reset =
new IdUserPasswordReset(
FAKE_USER.id(),
token,
OffsetDateTime.now().plusYears(1L)
);
Mockito.when(users.userPasswordResetGetForToken(token))
.thenReturn(Optional.of(reset));
final var requestId = UUID.randomUUID();
resets.resetCheck(
"127.0.0.1",
"NCSA Mosaic",
requestId,
Optional.of(token.value())
);
}
/**
* If a nonexistent token is provided the check fails.
*
* @throws Exception On errors
*/
@Test
public void testCheckNonexistent()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetCheck(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of(token.value())
);
});
assertEquals(PASSWORD_RESET_NONEXISTENT, ex.errorCode());
}
/**
* If an expired token is provided the check fails.
*
* @throws Exception On errors
*/
@Test
public void testCheckExpired()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
final var reset =
new IdUserPasswordReset(
FAKE_USER.id(),
token,
this.serverClock.now()
.minusYears(1L)
);
Mockito.when(users.userPasswordResetGetForToken(token))
.thenReturn(Optional.of(reset));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetCheck(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of(token.value())
);
});
assertEquals(PASSWORD_RESET_NONEXISTENT, ex.errorCode());
}
/**
* If the database fails, the check fails.
*
* @throws Exception On errors
*/
@Test
public void testCheckDatabaseFails()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(users.userPasswordResetGetForToken(Mockito.any()))
.thenThrow(new IdDatabaseException("Ouch", SQL_ERROR, Map.of(), empty()));
final var token =
IdToken.generate();
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetCheck(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of(token.value())
);
});
assertEquals(SQL_ERROR, ex.errorCode());
}
/**
* If a token isn't provided, the check fails.
*
* @throws Exception On errors
*/
@Test
public void testCheckTokenMissing()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(users.userPasswordResetGetForToken(Mockito.any()))
.thenThrow(new IdDatabaseException("Ouch", SQL_ERROR, Map.of(), empty()));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetCheck(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty()
);
});
assertEquals(HTTP_PARAMETER_NONEXISTENT, ex.errorCode());
}
/**
* If a token isn't valid (well-formed), the check fails.
*
* @throws Exception On errors
*/
@Test
public void testCheckTokenInvalid()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
Mockito.when(users.userPasswordResetGetForToken(Mockito.any()))
.thenThrow(new IdDatabaseException("Ouch", SQL_ERROR, Map.of(), empty()));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetCheck(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("this isn't what a token looks like")
);
});
assertEquals(HTTP_PARAMETER_INVALID, ex.errorCode());
}
/**
* If the correct token is provided, and the token has not expired, the
* confirmation succeeds.
*
* @throws Exception On errors
*/
@Test
public void testConfirmOK()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
final var reset =
new IdUserPasswordReset(
FAKE_USER.id(),
token,
OffsetDateTime.now().plusYears(1L)
);
Mockito.when(users.userPasswordResetGetForToken(token))
.thenReturn(Optional.of(reset));
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcd"),
Optional.of(token.value())
);
}
/**
* Missing tokens fail confirmations.
*
* @throws Exception On errors
*/
@Test
public void testConfirmMissingToken()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcd"),
Optional.empty()
);
});
assertEquals(HTTP_PARAMETER_NONEXISTENT, ex.errorCode());
}
/**
* Missing passwords fail confirmations.
*
* @throws Exception On errors
*/
@Test
public void testConfirmMissingPassword0()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.empty(),
Optional.of("abcd"),
Optional.of(IdToken.generate().value())
);
});
assertEquals(HTTP_PARAMETER_NONEXISTENT, ex.errorCode());
}
/**
* Missing passwords fail confirmations.
*
* @throws Exception On errors
*/
@Test
public void testConfirmMissingPassword1()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.empty(),
Optional.of(IdToken.generate().value())
);
});
assertEquals(HTTP_PARAMETER_NONEXISTENT, ex.errorCode());
}
/**
* Invalid tokens fail confirmations.
*
* @throws Exception On errors
*/
@Test
public void testConfirmInvalidToken()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcd"),
Optional.of("this isn't what a token looks like.")
);
});
assertEquals(HTTP_PARAMETER_INVALID, ex.errorCode());
}
/**
* Mismatches passwords fail confirmations.
*
* @throws Exception On errors
*/
@Test
public void testConfirmMismatchedPasswords()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcde"),
Optional.of(IdToken.generate().value())
);
});
assertEquals(PASSWORD_RESET_MISMATCH, ex.errorCode());
}
/**
* If no token exists in the database, confirmation fails.
*
* @throws Exception On errors
*/
@Test
public void testConfirmTokenMissing()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcd"),
Optional.of(token.value())
);
});
assertEquals(PASSWORD_RESET_NONEXISTENT, ex.errorCode());
}
/**
* If an expired token exists in the database, confirmation fails.
*
* @throws Exception On errors
*/
@Test
public void testConfirmTokenExpired()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
final var reset =
new IdUserPasswordReset(
FAKE_USER.id(),
token,
this.serverClock.now()
.minusYears(1L)
);
Mockito.when(users.userPasswordResetGetForToken(token))
.thenReturn(Optional.of(reset));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcd"),
Optional.of(token.value())
);
});
assertEquals(PASSWORD_RESET_NONEXISTENT, ex.errorCode());
}
/**
* If the database fails, confirmation fails.
*
* @throws Exception On errors
*/
@Test
public void testConfirmDatabaseFails()
throws Exception
{
final var resets =
this.createInstanceA();
final var connection =
Mockito.mock(IdDatabaseConnectionType.class);
final var transaction =
Mockito.mock(IdDatabaseTransactionType.class);
final var users =
Mockito.mock(IdDatabaseUsersQueriesType.class);
Mockito.when(this.database.openConnection(IDSTORE))
.thenReturn(connection);
Mockito.when(connection.openTransaction())
.thenReturn(transaction);
Mockito.when(transaction.queries(IdDatabaseUsersQueriesType.class))
.thenReturn(users);
final var token =
IdToken.generate();
Mockito.when(users.userPasswordResetGetForToken(token))
.thenThrow(new IdDatabaseException("Ouch", SQL_ERROR, Map.of(), empty()));
final var ex =
assertThrows(IdCommandExecutionFailure.class, () -> {
resets.resetConfirm(
"127.0.0.1",
"NCSA Mosaic",
UUID.randomUUID(),
Optional.of("abcd"),
Optional.of("abcd"),
Optional.of(token.value())
);
});
assertEquals(SQL_ERROR, ex.errorCode());
}
}