Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2018-09-21 10:29:21 +02:00
48 changed files with 1485 additions and 823 deletions

View File

@@ -1,207 +0,0 @@
/**
* Copyright (c) 2010, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.repository;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Lists;
import org.apache.commons.lang.StringEscapeUtils;
import sonia.scm.util.Util;
import java.util.List;
//~--- JDK imports ------------------------------------------------------------
/**
*
* @author Sebastian Sdorra
* @since 1.15
*/
public final class EscapeUtil
{
/**
* Constructs ...
*
*/
private EscapeUtil() {}
//~--- methods --------------------------------------------------------------
/**
* Method description
*
*
* @param result
*/
public static void escape(BrowserResult result)
{
result.setBranch(escape(result.getBranch()));
result.setTag(escape(result.getTag()));
for (FileObject fo : result)
{
escape(fo);
}
}
/**
* Method description
*
*
* @param result
* @since 1.17
*/
public static void escape(BlameResult result)
{
for (BlameLine line : result.getBlameLines())
{
escape(line);
}
}
/**
* Method description
*
*
* @param line
* @since 1.17
*/
public static void escape(BlameLine line)
{
line.setDescription(escape(line.getDescription()));
escape(line.getAuthor());
}
/**
* Method description
*
*
* @param fo
*/
public static void escape(FileObject fo)
{
fo.setDescription(escape(fo.getDescription()));
fo.setName(fo.getName());
fo.setPath(fo.getPath());
}
/**
* Method description
*
*
* @param changeset
*/
public static void escape(Changeset changeset)
{
changeset.setDescription(escape(changeset.getDescription()));
escape(changeset.getAuthor());
changeset.setBranches(escapeList(changeset.getBranches()));
changeset.setTags(escapeList(changeset.getTags()));
}
/**
* Method description
*
*
* @param person
* @since 1.17
*/
public static void escape(Person person)
{
if (person != null)
{
person.setName(escape(person.getName()));
person.setMail(escape(person.getMail()));
}
}
/**
* Method description
*
*
* @param result
*/
public static void escape(ChangesetPagingResult result)
{
for (Changeset c : result)
{
escape(c);
}
}
/**
* Method description
*
*
* @param value
*
* @return
*/
public static String escape(String value)
{
return StringEscapeUtils.escapeHtml(value);
}
/**
* Method description
*
*
* @param values
*
* @return
*/
public static List<String> escapeList(List<String> values)
{
if (Util.isNotEmpty(values))
{
List<String> newList = Lists.newArrayList();
for (String v : values)
{
newList.add(StringEscapeUtils.escapeHtml(v));
}
values = newList;
}
return values;
}
public static void escape(Modifications modifications) {
modifications.setAdded(escapeList(modifications.getAdded()));
modifications.setModified(escapeList(modifications.getModified()));
modifications.setRemoved(escapeList(modifications.getRemoved()));
}
}

View File

@@ -111,7 +111,6 @@ public class PreProcessorUtil
blameLine.getLineNumber(), repository.getName());
}
EscapeUtil.escape(blameLine);
handlePreProcess(repository,blameLine,blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
}
@@ -123,22 +122,6 @@ public class PreProcessorUtil
* @param blameResult
*/
public void prepareForReturn(Repository repository, BlameResult blameResult)
{
prepareForReturn(repository, blameResult, true);
}
/**
* Method description
*
*
* @param repository
* @param blameResult
* @param escape
*
* @since 1.35
*/
public void prepareForReturn(Repository repository, BlameResult blameResult,
boolean escape)
{
if (logger.isTraceEnabled())
{
@@ -146,10 +129,6 @@ public class PreProcessorUtil
repository.getName());
}
if (escape)
{
EscapeUtil.escape(blameResult);
}
handlePreProcessForIterable(repository, blameResult.getBlameLines(),blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
}
@@ -162,32 +141,12 @@ public class PreProcessorUtil
*/
public void prepareForReturn(Repository repository, Changeset changeset)
{
prepareForReturn(repository, changeset, true);
}
/**
* Method description
*
*
* @param repository
* @param changeset
* @param escape
*
* @since 1.35
*/
public void prepareForReturn(Repository repository, Changeset changeset, boolean escape){
logger.trace("prepare changeset {} of repository {} for return", changeset.getId(), repository.getName());
if (escape) {
EscapeUtil.escape(changeset);
}
handlePreProcess(repository, changeset, changesetPreProcessorFactorySet, changesetPreProcessorSet);
}
public void prepareForReturn(Repository repository, Modifications modifications, boolean escape) {
public void prepareForReturn(Repository repository, Modifications modifications) {
logger.trace("prepare modifications {} of repository {} for return", modifications, repository.getName());
if (escape) {
EscapeUtil.escape(modifications);
}
handlePreProcess(repository, modifications, modificationsPreProcessorFactorySet, modificationsPreProcessorSet);
}
@@ -199,22 +158,6 @@ public class PreProcessorUtil
* @param result
*/
public void prepareForReturn(Repository repository, BrowserResult result)
{
prepareForReturn(repository, result, true);
}
/**
* Method description
*
*
* @param repository
* @param result
* @param escape
*
* @since 1.35
*/
public void prepareForReturn(Repository repository, BrowserResult result,
boolean escape)
{
if (logger.isTraceEnabled())
{
@@ -222,10 +165,6 @@ public class PreProcessorUtil
repository.getName());
}
if (escape)
{
EscapeUtil.escape(result);
}
handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
}
@@ -235,12 +174,8 @@ public class PreProcessorUtil
*
* @param repository
* @param result
* @param escape
*
* @since 1.35
*/
public void prepareForReturn(Repository repository,
ChangesetPagingResult result, boolean escape)
public void prepareForReturn(Repository repository, ChangesetPagingResult result)
{
if (logger.isTraceEnabled())
{
@@ -248,27 +183,9 @@ public class PreProcessorUtil
repository.getName());
}
if (escape)
{
EscapeUtil.escape(result);
}
handlePreProcessForIterable(repository,result,changesetPreProcessorFactorySet, changesetPreProcessorSet);
}
/**
* Method description
*
*
* @param repository
* @param result
*/
public void prepareForReturn(Repository repository,
ChangesetPagingResult result)
{
prepareForReturn(repository, result, true);
}
private <T, F extends PreProcessorFactory<T>, P extends PreProcessor<T>> void handlePreProcess(Repository repository, T processedObject,
Collection<F> factories,
Collection<P> preProcessors) {

View File

@@ -185,7 +185,7 @@ public final class BlameCommandBuilder
if (!disablePreProcessors && (result != null))
{
preProcessorUtil.prepareForReturn(repository, result, !disableEscaping);
preProcessorUtil.prepareForReturn(repository, result);
}
return result;
@@ -210,24 +210,6 @@ public final class BlameCommandBuilder
return this;
}
/**
* Disable html escaping for the returned blame lines. By default all
* blame lines are html escaped.
*
*
* @param disableEscaping true to disable the html escaping
*
* @return {@code this}
*
* @since 1.35
*/
public BlameCommandBuilder setDisableEscaping(boolean disableEscaping)
{
this.disableEscaping = disableEscaping;
return this;
}
/**
* Disable the execution of pre processors.
*
@@ -362,9 +344,6 @@ public final class BlameCommandBuilder
/** the cache */
private final Cache<CacheKey, BlameResult> cache;
/** disable escaping */
private boolean disableEscaping = false;
/** disable change */
private boolean disableCache = false;

View File

@@ -179,7 +179,7 @@ public final class BrowseCommandBuilder
if (!disablePreProcessors && (result != null))
{
preProcessorUtil.prepareForReturn(repository, result, !disableEscaping);
preProcessorUtil.prepareForReturn(repository, result);
List<FileObject> fileObjects = result.getFiles();
@@ -212,24 +212,6 @@ public final class BrowseCommandBuilder
return this;
}
/**
* Disable html escaping for the returned file objects. By default all
* file objects are html escaped.
*
*
* @param disableEscaping true to disable the html escaping
*
* @return {@code this}
*
* @since 1.35
*/
public BrowseCommandBuilder setDisableEscaping(boolean disableEscaping)
{
this.disableEscaping = disableEscaping;
return this;
}
/**
* Disabling the last commit means that every call to
* {@link FileObject#getDescription()} and
@@ -433,9 +415,6 @@ public final class BrowseCommandBuilder
/** cache */
private final Cache<CacheKey, BrowserResult> cache;
/** disable escaping */
private boolean disableEscaping = false;
/** disables the cache */
private boolean disableCache = false;

View File

@@ -132,8 +132,7 @@ public final class HookChangesetBuilder
try
{
copy = DeepCopy.copy(c);
preProcessorUtil.prepareForReturn(repository, copy,
!disableEscaping);
preProcessorUtil.prepareForReturn(repository, copy);
}
catch (IOException ex)
{
@@ -156,24 +155,6 @@ public final class HookChangesetBuilder
//~--- set methods ----------------------------------------------------------
/**
* Disable html escaping for the returned changesets. By default all
* changesets are html escaped.
*
*
* @param disableEscaping true to disable the html escaping
*
* @return {@code this}
*
* @since 1.35
*/
public HookChangesetBuilder setDisableEscaping(boolean disableEscaping)
{
this.disableEscaping = disableEscaping;
return this;
}
/**
* Disable the execution of pre processors.
*
@@ -192,9 +173,6 @@ public final class HookChangesetBuilder
//~--- fields ---------------------------------------------------------------
/** disable escaping */
private boolean disableEscaping = false;
/** disable pre processors marker */
private boolean disablePreProcessors = false;

View File

@@ -264,7 +264,7 @@ public final class LogCommandBuilder
if (!disablePreProcessors && (cpr != null))
{
preProcessorUtil.prepareForReturn(repository, cpr, !disableEscaping);
preProcessorUtil.prepareForReturn(repository, cpr);
}
return cpr;
@@ -306,24 +306,6 @@ public final class LogCommandBuilder
return this;
}
/**
* Disable html escaping for the returned changesets. By default all
* changesets are html escaped.
*
*
* @param disableEscaping true to disable the html escaping
*
* @return {@code this}
*
* @since 1.35
*/
public LogCommandBuilder setDisableEscaping(boolean disableEscaping)
{
this.disableEscaping = disableEscaping;
return this;
}
/**
* Disable the execution of pre processors.
*
@@ -545,9 +527,6 @@ public final class LogCommandBuilder
/** repository to query */
private final Repository repository;
/** disable escaping */
private boolean disableEscaping = false;
/** disable cache */
private boolean disableCache = false;

View File

@@ -43,9 +43,6 @@ public final class ModificationsCommandBuilder {
private final PreProcessorUtil preProcessorUtil;
@Setter
private boolean disableEscaping = false;
@Setter
private boolean disableCache = false;
@@ -90,7 +87,7 @@ public final class ModificationsCommandBuilder {
}
}
if (!disablePreProcessors && (modifications != null)) {
preProcessorUtil.prepareForReturn(repository, modifications, !disableEscaping);
preProcessorUtil.prepareForReturn(repository, modifications);
}
return modifications;
}

View File

@@ -0,0 +1,11 @@
package sonia.scm.user;
public class ChangePasswordNotAllowedException extends RuntimeException {
public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password";
public ChangePasswordNotAllowedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,10 @@
package sonia.scm.user;
public class InvalidPasswordException extends RuntimeException {
public static final String INVALID_MATCHING = "The given Password does not match with the stored one.";
public InvalidPasswordException(String message) {
super(message);
}
}

View File

@@ -274,6 +274,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO
//J+
}
public User changePassword(String password){
setPassword(password);
return this;
}
//~--- get methods ----------------------------------------------------------
/**

View File

@@ -38,6 +38,11 @@ package sonia.scm.user;
import sonia.scm.Manager;
import sonia.scm.search.Searchable;
import java.text.MessageFormat;
import java.util.function.Consumer;
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
/**
* The central class for managing {@link User} objects.
* This class is a singleton and is available via injection.
@@ -68,4 +73,22 @@ public interface UserManager
* @since 1.14
*/
public String getDefaultType();
/**
* Only account of the default type "xml" can change their password
*/
default Consumer<User> getUserTypeChecker() {
return user -> {
if (!isTypeDefault(user)) {
throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType()));
}
};
}
default boolean isTypeDefault(User user) {
return getDefaultType().equals(user.getType());
}
}

View File

@@ -21,7 +21,7 @@ public class VndMediaType {
public static final String PERMISSION = PREFIX + "permission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;;
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
public static final String TAG = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;
public static final String BRANCH = PREFIX + "branch" + SUFFIX;
@@ -35,6 +35,8 @@ public class VndMediaType {
public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX;
public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX;
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
@SuppressWarnings("squid:S2068")
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;

View File

@@ -0,0 +1,79 @@
package sonia.scm.it;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import static org.assertj.core.api.Assertions.assertThat;
public class MeITCase {
@Before
public void init() {
TestData.cleanup();
}
@Test
public void adminShouldChangeOwnPassword() {
String newPassword = TestData.USER_SCM_ADMIN + "1";
// admin change the own password
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getMeResource()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo("xml"))
.requestChangePassword(TestData.USER_SCM_ADMIN, newPassword)
.assertStatusCode(204);
// assert password is changed -> login with the new Password than undo changes
ScmRequests.start()
.given()
.url(TestData.getUserUrl(TestData.USER_SCM_ADMIN))
.usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword)
.getMeResource()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin
.requestChangePassword(newPassword, TestData.USER_SCM_ADMIN)
.assertStatusCode(204);
}
@Test
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
String newUser = "user";
String password = "pass";
String type = "not XML Type";
TestData.createUser(newUser, password, true, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(200)
.usingMeResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getMeResource()
.assertStatusCode(403);
}
}

View File

@@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.PermissionType;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.repository.client.api.RepositoryClientException;
@@ -51,11 +53,11 @@ import java.util.Objects;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.TestData.callRepository;
import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN;
import static sonia.scm.it.utils.TestData.callRepository;
@RunWith(Parameterized.class)
public class PermissionsITCase {

View File

@@ -42,6 +42,8 @@ import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.client.api.RepositoryClient;
import sonia.scm.web.VndMediaType;
@@ -53,11 +55,11 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
import static sonia.scm.it.RegExMatcher.matchesPattern;
import static sonia.scm.it.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.TestData.repositoryJson;
import static sonia.scm.it.utils.RegExMatcher.matchesPattern;
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.TestData.repositoryJson;
@RunWith(Parameterized.class)
public class RepositoriesITCase {

View File

@@ -4,7 +4,6 @@ import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.apache.http.HttpStatus;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Maps;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
@@ -12,6 +11,9 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import sonia.scm.it.utils.RepositoryUtil;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.client.api.ClientCommand;
import sonia.scm.repository.client.api.RepositoryClient;
@@ -29,10 +31,10 @@ import static java.lang.Thread.sleep;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static sonia.scm.it.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD;
import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
@RunWith(Parameterized.class)
public class RepositoryAccessITCase {
@@ -42,7 +44,7 @@ public class RepositoryAccessITCase {
private final String repositoryType;
private File folder;
private RepositoryRequests.AppliedRepositoryGetRequest repositoryGetRequest;
private ScmRequests.AppliedRepositoryRequest repositoryGetRequest;
public RepositoryAccessITCase(String repositoryType) {
this.repositoryType = repositoryType;
@@ -57,12 +59,17 @@ public class RepositoryAccessITCase {
public void init() {
TestData.createDefault();
folder = tempFolder.getRoot();
repositoryGetRequest = RepositoryRequests.start()
repositoryGetRequest = ScmRequests.start()
.given()
.url(TestData.getDefaultRepositoryUrl(repositoryType))
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.get()
.getRepositoryResource()
.assertStatusCode(HttpStatus.SC_OK);
ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD)
.getMeResource();
}
@Test

View File

@@ -1,293 +0,0 @@
package sonia.scm.it;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* Encapsulate rest requests of a repository in builder pattern
* <p>
* A Get Request can be applied with the methods request*()
* These methods return a AppliedGet*Request object
* This object can be used to apply general assertions over the rest Assured response
* In the AppliedGet*Request classes there is a using*Response() method
* that return the *Response class containing specific operations related to the specific response
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
*/
public class RepositoryRequests {
private String url;
private String username;
private String password;
static RepositoryRequests start() {
return new RepositoryRequests();
}
Given given() {
return new Given();
}
/**
* Apply a GET Request to the extracted url from the given link
*
* @param linkPropertyName the property name of link
* @param response the response containing the link
* @return the response of the GET request using the given link
*/
private Response getResponseFromLink(Response response, String linkPropertyName) {
return getResponse(response
.then()
.extract()
.path(linkPropertyName));
}
/**
* Apply a GET Request to the given <code>url</code> and return the response.
*
* @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code>
*/
private Response getResponse(String url) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.get(url);
}
private void setUrl(String url) {
this.url = url;
}
private void setUsername(String username) {
this.username = username;
}
private void setPassword(String password) {
this.password = password;
}
private String getUrl() {
return url;
}
private String getUsername() {
return username;
}
private String getPassword() {
return password;
}
class Given {
GivenUrl url(String url) {
setUrl(url);
return new GivenUrl();
}
}
class GivenWithUrlAndAuth {
AppliedRepositoryGetRequest get() {
return new AppliedRepositoryGetRequest(
getResponse(url)
);
}
}
class AppliedGetRequest<SELF extends AppliedGetRequest> {
private Response response;
public AppliedGetRequest(Response response) {
this.response = response;
}
/**
* apply custom assertions to the actual response
*
* @param consumer consume the response in order to assert the content. the header, the payload etc..
* @return the self object
*/
SELF assertResponse(Consumer<Response> consumer) {
consumer.accept(response);
return (SELF) this;
}
/**
* special assertion of the status code
*
* @param expectedStatusCode the expected status code
* @return the self object
*/
SELF assertStatusCode(int expectedStatusCode) {
this.response.then().assertThat().statusCode(expectedStatusCode);
return (SELF) this;
}
}
class AppliedRepositoryGetRequest extends AppliedGetRequest<AppliedRepositoryGetRequest> {
AppliedRepositoryGetRequest(Response response) {
super(response);
}
RepositoryResponse usingRepositoryResponse() {
return new RepositoryResponse(super.response);
}
}
class RepositoryResponse {
private Response repositoryResponse;
public RepositoryResponse(Response repositoryResponse) {
this.repositoryResponse = repositoryResponse;
}
AppliedGetSourcesRequest requestSources() {
return new AppliedGetSourcesRequest(getResponseFromLink(repositoryResponse, "_links.sources.href"));
}
AppliedGetChangesetsRequest requestChangesets() {
return new AppliedGetChangesetsRequest(getResponseFromLink(repositoryResponse, "_links.changesets.href"));
}
}
class AppliedGetChangesetsRequest extends AppliedGetRequest<AppliedGetChangesetsRequest> {
AppliedGetChangesetsRequest(Response response) {
super(response);
}
ChangesetsResponse usingChangesetsResponse() {
return new ChangesetsResponse(super.response);
}
}
class ChangesetsResponse {
private Response changesetsResponse;
public ChangesetsResponse(Response changesetsResponse) {
this.changesetsResponse = changesetsResponse;
}
ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
changesetsConsumer.accept(changesets);
return this;
}
AppliedGetDiffRequest requestDiff(String revision) {
return new AppliedGetDiffRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
}
public AppliedGetModificationsRequest requestModifications(String revision) {
return new AppliedGetModificationsRequest(getResponseFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
}
}
class AppliedGetSourcesRequest extends AppliedGetRequest<AppliedGetSourcesRequest> {
public AppliedGetSourcesRequest(Response sourcesResponse) {
super(sourcesResponse);
}
SourcesResponse usingSourcesResponse() {
return new SourcesResponse(super.response);
}
}
class SourcesResponse {
private Response sourcesResponse;
SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
}
SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files");
assertFiles.accept(files);
return this;
}
AppliedGetChangesetsRequest requestFileHistory(String fileName) {
return new AppliedGetChangesetsRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
}
AppliedGetSourcesRequest requestSelf(String fileName) {
return new AppliedGetSourcesRequest(getResponseFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
}
}
class AppliedGetDiffRequest extends AppliedGetRequest<AppliedGetDiffRequest> {
AppliedGetDiffRequest(Response response) {
super(response);
}
}
class GivenUrl {
GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
setUsername(username);
setPassword(password);
return new GivenWithUrlAndAuth();
}
}
class AppliedGetModificationsRequest extends AppliedGetRequest<AppliedGetModificationsRequest> {
public AppliedGetModificationsRequest(Response response) { super(response); }
ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String > added = resource.then().extract().path("added");
assertAdded.accept(added);
return this;
}
ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String > removed = resource.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String > modified = resource.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
}

View File

@@ -0,0 +1,113 @@
package sonia.scm.it;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.it.utils.ScmRequests;
import sonia.scm.it.utils.TestData;
import static org.assertj.core.api.Assertions.assertThat;
public class UserITCase {
@Before
public void init(){
TestData.cleanup();
}
@Test
public void adminShouldChangeOwnPassword() {
String newUser = "user";
String password = "pass";
TestData.createUser(newUser, password, true, "xml");
String newPassword = "new_password";
// admin change the own password
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204);
// assert password is changed -> login with the new Password
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, newPassword)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull);
}
@Test
public void adminShouldChangePasswordOfOtherUser() {
String newUser = "user";
String password = "pass";
TestData.createUser(newUser, password, true, "xml");
String newPassword = "new_password";
// admin change the password of the user
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))// the admin get the user object
.usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin
.assertPassword(Assert::assertNull)
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
.assertStatusCode(204);
// assert password is changed
ScmRequests.start()
.given()
.url(TestData.getUserUrl(newUser))
.usernameAndPassword(newUser, newPassword)
.getUserResource()
.assertStatusCode(200);
}
@Test
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
String newUser = "user";
String password = "pass";
String type = "not XML Type";
TestData.createUser(newUser, password, true, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(200)
.usingUserResponse()
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
.assertPassword(Assert::assertNull)
.assertType(s -> assertThat(s).isEqualTo(type))
.assertPasswordLinkDoesNotExists();
}
@Test
public void shouldGet403IfUserIsNotAdmin() {
String newUser = "user";
String password = "pass";
String type = "xml";
TestData.createUser(newUser, password, false, type);
ScmRequests.start()
.given()
.url(TestData.getMeUrl())
.usernameAndPassword(newUser, password)
.getUserResource()
.assertStatusCode(403);
}
}

View File

@@ -0,0 +1,95 @@
package sonia.scm.it.utils;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonValue;
import java.math.BigDecimal;
import java.math.BigInteger;
public class NullAwareJsonObjectBuilder implements JsonObjectBuilder {
public static JsonObjectBuilder wrap(JsonObjectBuilder builder) {
if (builder == null) {
throw new IllegalArgumentException("Can't wrap nothing.");
}
return new NullAwareJsonObjectBuilder(builder);
}
private final JsonObjectBuilder builder;
private NullAwareJsonObjectBuilder(JsonObjectBuilder builder) {
this.builder = builder;
}
public JsonObjectBuilder add(String name, JsonValue value) {
return builder.add(name, (value == null) ? JsonValue.NULL : value);
}
@Override
public JsonObjectBuilder add(String name, String value) {
if (value != null){
return builder.add(name, value );
}else{
return builder.addNull(name);
}
}
@Override
public JsonObjectBuilder add(String name, BigInteger value) {
if (value != null){
return builder.add(name, value );
}else{
return builder.addNull(name);
}
}
@Override
public JsonObjectBuilder add(String name, BigDecimal value) {
if (value != null){
return builder.add(name, value );
}else{
return builder.addNull(name);
}
}
@Override
public JsonObjectBuilder add(String s, int i) {
return builder.add(s, i);
}
@Override
public JsonObjectBuilder add(String s, long l) {
return builder.add(s, l);
}
@Override
public JsonObjectBuilder add(String s, double v) {
return builder.add(s, v);
}
@Override
public JsonObjectBuilder add(String s, boolean b) {
return builder.add(s, b);
}
@Override
public JsonObjectBuilder addNull(String s) {
return builder.addNull(s);
}
@Override
public JsonObjectBuilder add(String s, JsonObjectBuilder jsonObjectBuilder) {
return builder.add(s, jsonObjectBuilder);
}
@Override
public JsonObjectBuilder add(String s, JsonArrayBuilder jsonArrayBuilder) {
return builder.add(s, jsonArrayBuilder);
}
@Override
public JsonObject build() {
return builder.build();
}
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
@@ -6,7 +6,7 @@ import org.hamcrest.Matcher;
import java.util.regex.Pattern;
class RegExMatcher extends BaseMatcher<String> {
public class RegExMatcher extends BaseMatcher<String> {
public static Matcher<String> matchesPattern(String pattern) {
return new RegExMatcher(pattern);
}

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
@@ -24,11 +24,11 @@ public class RepositoryUtil {
private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory();
static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
public static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException {
return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin");
}
static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException {
String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK)
.extract()
.path("_links.protocol.find{it.name=='http'}.href");
@@ -36,14 +36,14 @@ public class RepositoryUtil {
return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder);
}
static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException {
String uuid = UUID.randomUUID().toString();
String name = "file-" + uuid + ".uuid";
createAndCommitFile(client, username, name, uuid);
return name;
}
static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
public static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException {
writeAndAddFile(repositoryClient, fileName, content);
return commit(repositoryClient, username, "added " + fileName);
}
@@ -59,7 +59,7 @@ public class RepositoryUtil {
* @return the changeset with all modifications
* @throws IOException
*/
static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
public static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map<String, String> addedFiles, Map<String, String> modifiedFiles, List<String> removedFiles) throws IOException {
for (String fileName : addedFiles.keySet()) {
writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName));
}
@@ -80,7 +80,7 @@ public class RepositoryUtil {
return file;
}
static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException {
deleteFileAndApplyRemoveCommand(repositoryClient, fileName);
return commit(repositoryClient, username, "removed " + fileName);
}
@@ -115,7 +115,7 @@ public class RepositoryUtil {
return changeset;
}
static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
public static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException {
if (repositoryClient.isCommandSupported(ClientCommand.TAG)) {
Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN);
if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) {

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import io.restassured.RestAssured;
import io.restassured.specification.RequestSpecification;

View File

@@ -0,0 +1,465 @@
package sonia.scm.it.utils;
import io.restassured.RestAssured;
import io.restassured.response.Response;
import sonia.scm.web.VndMediaType;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import static org.hamcrest.Matchers.is;
import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
/**
* Encapsulate rest requests of a repository in builder pattern
* <p>
* A Get Request can be applied with the methods request*()
* These methods return a AppliedGet*Request object
* This object can be used to apply general assertions over the rest Assured response
* In the AppliedGet*Request classes there is a using*Response() method
* that return the *Response class containing specific operations related to the specific response
* the *Response class contains also the request*() method to apply the next GET request from a link in the response.
*/
public class ScmRequests {
private String url;
private String username;
private String password;
public static ScmRequests start() {
return new ScmRequests();
}
public Given given() {
return new Given();
}
/**
* Apply a GET Request to the extracted url from the given link
*
* @param linkPropertyName the property name of link
* @param response the response containing the link
* @return the response of the GET request using the given link
*/
private Response applyGETRequestFromLink(Response response, String linkPropertyName) {
return applyGETRequest(response
.then()
.extract()
.path(linkPropertyName));
}
/**
* Apply a GET Request to the given <code>url</code> and return the response.
*
* @param url the url of the GET request
* @return the response of the GET request using the given <code>url</code>
*/
private Response applyGETRequest(String url) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.get(url);
}
/**
* Apply a PUT Request to the extracted url from the given link
*
* @param response the response containing the link
* @param linkPropertyName the property name of link
* @param body
* @return the response of the PUT request using the given link
*/
private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) {
return applyPUTRequest(response
.then()
.extract()
.path(linkPropertyName), content, body);
}
/**
* Apply a PUT Request to the given <code>url</code> and return the response.
*
* @param url the url of the PUT request
* @param mediaType
* @param body
* @return the response of the PUT request using the given <code>url</code>
*/
private Response applyPUTRequest(String url, String mediaType, String body) {
return RestAssured.given()
.auth().preemptive().basic(username, password)
.when()
.contentType(mediaType)
.accept(mediaType)
.body(body)
.put(url);
}
private void setUrl(String url) {
this.url = url;
}
private void setUsername(String username) {
this.username = username;
}
private void setPassword(String password) {
this.password = password;
}
private String getUrl() {
return url;
}
private String getUsername() {
return username;
}
private String getPassword() {
return password;
}
public class Given {
public GivenUrl url(String url) {
setUrl(url);
return new GivenUrl();
}
public GivenUrl url(URI url) {
setUrl(url.toString());
return new GivenUrl();
}
}
public class GivenWithUrlAndAuth {
public AppliedMeRequest getMeResource() {
return new AppliedMeRequest(applyGETRequest(url));
}
public AppliedUserRequest getUserResource() {
return new AppliedUserRequest(applyGETRequest(url));
}
public AppliedRepositoryRequest getRepositoryResource() {
return new AppliedRepositoryRequest(
applyGETRequest(url)
);
}
}
public class AppliedRequest<SELF extends AppliedRequest> {
private Response response;
public AppliedRequest(Response response) {
this.response = response;
}
/**
* apply custom assertions to the actual response
*
* @param consumer consume the response in order to assert the content. the header, the payload etc..
* @return the self object
*/
public SELF assertResponse(Consumer<Response> consumer) {
consumer.accept(response);
return (SELF) this;
}
/**
* special assertion of the status code
*
* @param expectedStatusCode the expected status code
* @return the self object
*/
public SELF assertStatusCode(int expectedStatusCode) {
this.response.then().assertThat().statusCode(expectedStatusCode);
return (SELF) this;
}
}
public class AppliedRepositoryRequest extends AppliedRequest<AppliedRepositoryRequest> {
public AppliedRepositoryRequest(Response response) {
super(response);
}
public RepositoryResponse usingRepositoryResponse() {
return new RepositoryResponse(super.response);
}
}
public class RepositoryResponse {
private Response repositoryResponse;
public RepositoryResponse(Response repositoryResponse) {
this.repositoryResponse = repositoryResponse;
}
public AppliedSourcesRequest requestSources() {
return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href"));
}
public AppliedChangesetsRequest requestChangesets() {
return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href"));
}
}
public class AppliedChangesetsRequest extends AppliedRequest<AppliedChangesetsRequest> {
public AppliedChangesetsRequest(Response response) {
super(response);
}
public ChangesetsResponse usingChangesetsResponse() {
return new ChangesetsResponse(super.response);
}
}
public class ChangesetsResponse {
private Response changesetsResponse;
public ChangesetsResponse(Response changesetsResponse) {
this.changesetsResponse = changesetsResponse;
}
public ChangesetsResponse assertChangesets(Consumer<List<Map>> changesetsConsumer) {
List<Map> changesets = changesetsResponse.then().extract().path("_embedded.changesets");
changesetsConsumer.accept(changesets);
return this;
}
public AppliedDiffRequest requestDiff(String revision) {
return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href"));
}
public AppliedModificationsRequest requestModifications(String revision) {
return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href"));
}
}
public class AppliedSourcesRequest extends AppliedRequest<AppliedSourcesRequest> {
public AppliedSourcesRequest(Response sourcesResponse) {
super(sourcesResponse);
}
public SourcesResponse usingSourcesResponse() {
return new SourcesResponse(super.response);
}
}
public class SourcesResponse {
private Response sourcesResponse;
public SourcesResponse(Response sourcesResponse) {
this.sourcesResponse = sourcesResponse;
}
public SourcesResponse assertRevision(Consumer<String> assertRevision) {
String revision = sourcesResponse.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public SourcesResponse assertFiles(Consumer<List> assertFiles) {
List files = sourcesResponse.then().extract().path("files");
assertFiles.accept(files);
return this;
}
public AppliedChangesetsRequest requestFileHistory(String fileName) {
return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href"));
}
public AppliedSourcesRequest requestSelf(String fileName) {
return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href"));
}
}
public class AppliedDiffRequest extends AppliedRequest<AppliedDiffRequest> {
public AppliedDiffRequest(Response response) {
super(response);
}
}
public class GivenUrl {
public GivenWithUrlAndAuth usernameAndPassword(String username, String password) {
setUsername(username);
setPassword(password);
return new GivenWithUrlAndAuth();
}
}
public class AppliedModificationsRequest extends AppliedRequest<AppliedModificationsRequest> {
public AppliedModificationsRequest(Response response) {
super(response);
}
public ModificationsResponse usingModificationsResponse() {
return new ModificationsResponse(super.response);
}
}
public class ModificationsResponse {
private Response resource;
public ModificationsResponse(Response resource) {
this.resource = resource;
}
public ModificationsResponse assertRevision(Consumer<String> assertRevision) {
String revision = resource.then().extract().path("revision");
assertRevision.accept(revision);
return this;
}
public ModificationsResponse assertAdded(Consumer<List<String>> assertAdded) {
List<String> added = resource.then().extract().path("added");
assertAdded.accept(added);
return this;
}
public ModificationsResponse assertRemoved(Consumer<List<String>> assertRemoved) {
List<String> removed = resource.then().extract().path("removed");
assertRemoved.accept(removed);
return this;
}
public ModificationsResponse assertModified(Consumer<List<String>> assertModified) {
List<String> modified = resource.then().extract().path("modified");
assertModified.accept(modified);
return this;
}
}
public class AppliedMeRequest extends AppliedRequest<AppliedMeRequest> {
public AppliedMeRequest(Response response) {
super(response);
}
public MeResponse usingMeResponse() {
return new MeResponse(super.response);
}
}
public class MeResponse extends UserResponse<MeResponse> {
public MeResponse(Response response) {
super(response);
}
public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)));
}
}
public class UserResponse<SELF extends UserResponse> extends ModelResponse<SELF> {
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
public UserResponse(Response response) {
super(response);
}
public SELF assertPassword(Consumer<String> assertPassword) {
return super.assertSingleProperty(assertPassword, "password");
}
public SELF assertType(Consumer<String> assertType) {
return assertSingleProperty(assertType, "type");
}
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
return assertSingleProperty(assertAdmin, "admin");
}
public SELF assertPasswordLinkDoesNotExists() {
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
}
public SELF assertPasswordLinkExists() {
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
}
public AppliedChangePasswordRequest requestChangePassword(String newPassword) {
return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword)));
}
}
/**
* encapsulate standard assertions over model properties
*/
public class ModelResponse<SELF extends ModelResponse> {
protected Response response;
public ModelResponse(Response response) {
this.response = response;
}
public <T> SELF assertSingleProperty(Consumer<T> assertSingleProperty, String propertyJsonPath) {
T propertyValue = response.then().extract().path(propertyJsonPath);
assertSingleProperty.accept(propertyValue);
return (SELF) this;
}
public SELF assertPropertyPathExists(String propertyJsonPath) {
response.then().assertThat().body("any { it.containsKey('" + propertyJsonPath + "')}", is(true));
return (SELF) this;
}
public SELF assertPropertyPathDoesNotExists(String propertyJsonPath) {
response.then().assertThat().body("this.any { it.containsKey('" + propertyJsonPath + "')}", is(false));
return (SELF) this;
}
public SELF assertArrayProperty(Consumer<List> assertProperties, String propertyJsonPath) {
List properties = response.then().extract().path(propertyJsonPath);
assertProperties.accept(properties);
return (SELF) this;
}
}
public class AppliedChangePasswordRequest extends AppliedRequest<AppliedChangePasswordRequest> {
public AppliedChangePasswordRequest(Response response) {
super(response);
}
}
public class AppliedUserRequest extends AppliedRequest<AppliedUserRequest> {
public AppliedUserRequest(Response response) {
super(response);
}
public UserResponse usingUserResponse() {
return new UserResponse(super.response);
}
}
}

View File

@@ -1,12 +1,12 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import sonia.scm.util.IOUtil;
import java.util.ArrayList;
import java.util.Collection;
class ScmTypes {
static Collection<String> availableScmTypes() {
public class ScmTypes {
public static Collection<String> availableScmTypes() {
Collection<String> params = new ArrayList<>();
params.add("git");

View File

@@ -1,4 +1,4 @@
package sonia.scm.it;
package sonia.scm.it.utils;
import io.restassured.response.ValidatableResponse;
import org.apache.http.HttpStatus;
@@ -8,14 +8,16 @@ import sonia.scm.repository.PermissionType;
import sonia.scm.web.VndMediaType;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Arrays.asList;
import static sonia.scm.it.RestUtil.createResourceUrl;
import static sonia.scm.it.RestUtil.given;
import static sonia.scm.it.ScmTypes.availableScmTypes;
import static sonia.scm.it.utils.RestUtil.createResourceUrl;
import static sonia.scm.it.utils.RestUtil.given;
import static sonia.scm.it.utils.ScmTypes.availableScmTypes;
public class TestData {
@@ -26,6 +28,7 @@ public class TestData {
private static final List<String> PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS);
private static Map<String, String> DEFAULT_REPOSITORIES = new HashMap<>();
public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder());
public static void createDefault() {
cleanup();
@@ -44,27 +47,31 @@ public class TestData {
}
public static void createUser(String username, String password) {
createUser(username, password, false, "xml");
}
public static void createUser(String username, String password, boolean isAdmin, String type) {
LOG.info("create user with username: {}", username);
String admin = isAdmin ? "true" : "false";
given(VndMediaType.USER)
.when()
.content(" {\n" +
" \"active\": true,\n" +
" \"admin\": false,\n" +
" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" +
" \"displayName\": \"" + username + "\",\n" +
" \"mail\": \"user1@scm-manager.org\",\n" +
" \"name\": \"" + username + "\",\n" +
" \"password\": \"" + password + "\",\n" +
" \"type\": \"xml\"\n" +
" \n" +
" }")
.post(createResourceUrl("users"))
.content(new StringBuilder()
.append(" {\n")
.append(" \"active\": true,\n")
.append(" \"admin\": ").append(admin).append(",\n")
.append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n")
.append(" \"displayName\": \"").append(username).append("\",\n")
.append(" \"mail\": \"user1@scm-manager.org\",\n")
.append(" \"name\": \"").append(username).append("\",\n")
.append(" \"password\": \"").append(password).append("\",\n")
.append(" \"type\": \"").append(type).append("\"\n")
.append(" }").toString())
.post(getUsersUrl())
.then()
.statusCode(HttpStatus.SC_CREATED)
;
}
public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) {
String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType);
LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl);
@@ -183,7 +190,7 @@ public class TestData {
}
public static String repositoryJson(String repositoryType) {
return Json.createObjectBuilder()
return JSON_BUILDER
.add("contact", "zaphod.beeblebrox@hitchhiker.com")
.add("description", "Heart of Gold")
.add("name", "HeartOfGold-" + repositoryType)
@@ -192,6 +199,29 @@ public class TestData {
.build().toString();
}
public static URI getMeUrl() {
return RestUtil.createResourceUrl("me/");
}
public static URI getUsersUrl() {
return RestUtil.createResourceUrl("users/");
}
public static URI getUserUrl(String username) {
return getUsersUrl().resolve(username);
}
public static String createPasswordChangeJson(String oldPassword, String newPassword) {
return JSON_BUILDER
.add("oldPassword", oldPassword)
.add("newPassword", newPassword)
.build().toString();
}
public static void main(String[] args) {
cleanup();
}

View File

@@ -278,7 +278,6 @@ public class GitRepositoryViewer
{
//J-
ChangesetPagingResult cpr = service.getLogCommand()
.setDisableEscaping(true)
.setBranch(name)
.setPagingLimit(CHANGESET_PER_BRANCH)
.getChangesets();

View File

@@ -56,7 +56,7 @@ const userZaphod = {
displayName: "Z. Beeblebrox",
mail: "president@heartofgold.universe",
name: "zaphod",
password: "__dummypassword__",
password: "",
type: "xml",
properties: {},
_links: {
@@ -79,7 +79,7 @@ const userFord = {
displayName: "F. Prefect",
mail: "ford@prefect.universe",
name: "ford",
password: "__dummypassword__",
password: "",
type: "xml",
properties: {},
_links: {

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.user.ChangePasswordNotAllowedException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class ChangePasswordNotAllowedExceptionMapper implements ExceptionMapper<ChangePasswordNotAllowedException> {
@Override
public Response toResponse(ChangePasswordNotAllowedException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -10,6 +10,7 @@ import sonia.scm.PageResult;
import javax.ws.rs.core.Response;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -37,6 +38,15 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
return singleAdapter.get(loadBy(id), mapToDto);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
return singleAdapter.update(
loadBy(id),
applyChanges,
idStaysTheSame(id),
checker
);
}
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws NotFoundException, ConcurrentModificationException {
return singleAdapter.update(
loadBy(id),

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import sonia.scm.user.InvalidPasswordException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidPasswordExceptionMapper implements ExceptionMapper<InvalidPasswordException> {
@Override
public Response toResponse(InvalidPasswordException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(exception.getMessage())
.build();
}
}

View File

@@ -8,6 +8,7 @@ public class MapperModule extends AbstractModule {
@Override
protected void configure() {
bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass());
bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass());
bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass());
bind(UserCollectionToDtoMapper.class);

View File

@@ -4,19 +4,27 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.user.InvalidPasswordException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.function.Consumer;
import static sonia.scm.user.InvalidPasswordException.INVALID_MATCHING;
/**
@@ -24,15 +32,20 @@ import javax.ws.rs.core.UriInfo;
*/
@Path(MeResource.ME_PATH_V2)
public class MeResource {
static final String ME_PATH_V2 = "v2/me/";
public static final String ME_PATH_V2 = "v2/me/";
private final UserToUserDtoMapper userToDtoMapper;
private final MeToUserDtoMapper meToUserDtoMapper;
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
private final UserManager userManager;
@Inject
public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) {
this.userToDtoMapper = userToDtoMapper;
public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) {
this.meToUserDtoMapper = meToUserDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.passwordService = passwordService;
this.userManager = manager;
}
/**
@@ -50,6 +63,34 @@ public class MeResource {
public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException {
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.get(id, userToDtoMapper::map);
return adapter.get(id, meToUserDtoMapper::map);
}
/**
* Change password of the current user
*/
@PUT
@Path("password")
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
@Consumes(VndMediaType.PASSWORD_CHANGE)
public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker().andThen(getOldOriginalPasswordChecker(passwordChangeDto.getOldPassword())));
}
/**
* Match given old password from the dto with the stored password before updating
*/
private Consumer<User> getOldOriginalPasswordChecker(String oldPassword) {
return user -> {
if (!user.getPassword().equals(passwordService.encryptPassword(oldPassword))) {
throw new InvalidPasswordException(INVALID_MATCHING);
}
};
}
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class MeToUserDtoMapper extends UserToUserDtoMapper{
@Inject
private UserManager userManager;
@Inject
private ResourceLinks resourceLinks;
@Override
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self());
if (UserPermissions.delete(user).isPermitted()) {
linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName())));
}
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.me().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
target.add(linksBuilder.build());
}
}

View File

@@ -0,0 +1,17 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.NotEmpty;
@Getter
@Setter
@ToString
public class PasswordChangeDto {
private String oldPassword;
@NotEmpty
private String newPassword;
}

View File

@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
import sonia.scm.repository.NamespaceAndName;
import javax.inject.Inject;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
class ResourceLinks {
@@ -85,7 +86,42 @@ class ResourceLinks {
String update(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("update").parameters().href();
}
public String passwordChange(String name) {
return userLinkBuilder.method("getUserResource").parameters(name).method("changePassword").parameters().href();
}
}
MeLinks me() {
return new MeLinks(scmPathInfoStore.get(), this.user());
}
static class MeLinks {
private final LinkBuilder meLinkBuilder;
private UserLinks userLinks;
MeLinks(ScmPathInfo pathInfo, UserLinks user) {
meLinkBuilder = new LinkBuilder(pathInfo, MeResource.class);
userLinks = user;
}
String self() {
return meLinkBuilder.method("get").parameters().href();
}
String delete(String name) {
return userLinks.delete(name);
}
String update(String name) {
return userLinks.update(name);
}
public String passwordChange() {
return meLinkBuilder.method("changePassword").parameters().href();
}
}
UserCollectionLinks userCollection() {
return new UserCollectionLinks(scmPathInfoStore.get());

View File

@@ -11,6 +11,7 @@ import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -53,6 +54,11 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
.map(Response.ResponseBuilder::build)
.orElseThrow(NotFoundException::new);
}
public Response update(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges, Predicate<MODEL_OBJECT> hasSameKey, Consumer<MODEL_OBJECT> checker) throws NotFoundException, ConcurrentModificationException {
MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new);
checker.accept(existingModelObject);
return update(reader,applyChanges,hasSameKey);
}
/**
* Update the model object for the given id according to the given function and returns a corresponding http response.

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.AlreadyExistsException;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
@@ -29,14 +30,16 @@ public class UserCollectionResource {
private final ResourceLinks resourceLinks;
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final PasswordService passwordService;
@Inject
public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper,
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) {
UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks, PasswordService passwordService) {
this.dtoToUserMapper = dtoToUserMapper;
this.userCollectionToDtoMapper = userCollectionToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.resourceLinks = resourceLinks;
this.passwordService = passwordService;
}
/**
@@ -89,8 +92,6 @@ public class UserCollectionResource {
@TypeHint(TypeHint.NO_CONTENT.class)
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user"))
public Response create(@Valid UserDto userDto) throws AlreadyExistsException {
return adapter.create(userDto,
() -> dtoToUserMapper.map(userDto, ""),
user -> resourceLinks.user().self(user.getName()));
return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName()));
}
}

View File

@@ -26,6 +26,7 @@ public class UserDto extends HalRepresentation {
private String mail;
@Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$")
private String name;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String password;
private String type;
private Map<String, String> properties;

View File

@@ -1,37 +1,35 @@
package sonia.scm.api.v2.resources;
import org.apache.shiro.authc.credential.PasswordService;
import org.mapstruct.AfterMapping;
import org.mapstruct.Context;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.MappingTarget;
import sonia.scm.user.User;
import javax.inject.Inject;
import java.time.Instant;
import static sonia.scm.api.rest.resources.UserResource.DUMMY_PASSWORT;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
@Mapper
public abstract class UserDtoToUserMapper extends BaseDtoMapper {
@Inject
private PasswordService passwordService;
@Mapping(source = "password", target = "password", qualifiedByName = "encrypt")
@Mapping(target = "creationDate", ignore = true)
public abstract User map(UserDto userDto, @Context String originalPassword);
public abstract User map(UserDto userDto, @Context String usedPassword);
@Named("encrypt")
String encrypt(String password, @Context String originalPassword) {
if (DUMMY_PASSWORT.equals(password)) {
return originalPassword;
} else {
return passwordService.encryptPassword(password);
}
/**
* depends on the use case the right password will be mapped.
* The given Password in the context parameter will be set.
* The mapper consumer have the control of what password should be set.
* </p>
* eg. for update user action the password will be set to the original password
* for create user and change password actions the password is the user input
*
* @param usedPassword the password to be set
* @param user the target
*/
@AfterMapping
void overridePassword(@MappingTarget User user, @Context String usedPassword) {
user.setPassword(usedPassword);
}
}

View File

@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint;
import org.apache.shiro.authc.credential.PasswordService;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.user.User;
@@ -26,12 +27,16 @@ public class UserResource {
private final UserToUserDtoMapper userToDtoMapper;
private final IdResourceManagerAdapter<User, UserDto> adapter;
private final UserManager userManager;
private final PasswordService passwordService;
@Inject
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) {
public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) {
this.dtoToUserMapper = dtoToUserMapper;
this.userToDtoMapper = userToDtoMapper;
this.adapter = new IdResourceManagerAdapter<>(manager, User.class);
this.userManager = manager;
this.passwordService = passwordService;
}
/**
@@ -40,7 +45,6 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param id the id/name of the user
*
*/
@GET
@Path("")
@@ -63,7 +67,6 @@ public class UserResource {
* <strong>Note:</strong> This method requires "user" privilege.
*
* @param name the name of the user to delete.
*
*/
@DELETE
@Path("")
@@ -80,6 +83,7 @@ public class UserResource {
/**
* Modifies the given user.
* The given Password in the payload will be ignored. To Change Password use the changePassword endpoint
*
* <strong>Note:</strong> This method requires "user" privilege.
*
@@ -101,4 +105,30 @@ public class UserResource {
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
}
/**
* This Endpoint is for Admin user to modify a user password.
* The oldPassword property of the DTO is not needed here. it will be ignored.
* The oldPassword property is needed in the MeResources when the actual user change the own password.
*
* <strong>Note:</strong> This method requires "user:modify" privilege.
* @param name name of the user to be modified
* @param passwordChangeDto change password object to modify password. the old password is here not required
*/
@PUT
@Path("password")
@Consumes(VndMediaType.PASSWORD_CHANGE)
@StatusCodes({
@ResponseCode(code = 204, condition = "update success"),
@ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"),
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"),
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(TypeHint.NO_CONTENT.class)
public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException {
return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker());
}
}

View File

@@ -4,9 +4,10 @@ import com.google.common.annotations.VisibleForTesting;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import sonia.scm.api.rest.resources.UserResource;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import javax.inject.Inject;
@@ -19,19 +20,17 @@ import static de.otto.edison.hal.Links.linkingTo;
@Mapper
public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
@Inject
private UserManager userManager;
@Override
@Mapping(target = "attributes", ignore = true)
@Mapping(target = "password", ignore = true)
public abstract UserDto map(User modelObject);
@Inject
private ResourceLinks resourceLinks;
@VisibleForTesting
void setResourceLinks(ResourceLinks resourceLinks) {
this.resourceLinks = resourceLinks;
}
@AfterMapping
void removePassword(@MappingTarget UserDto target) {
target.setPassword(UserResource.DUMMY_PASSWORT);
}
@AfterMapping
protected void appendLinks(User user, @MappingTarget UserDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName()));
@@ -41,6 +40,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper<User, UserDto> {
if (UserPermissions.modify(user).isPermitted()) {
linksBuilder.single(link("update", resourceLinks.user().update(target.getName())));
}
if (userManager.isTypeDefault(user)) {
linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName())));
}
target.add(linksBuilder.build());
}

View File

@@ -15,6 +15,8 @@ public class DispatcherMock {
dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class);
dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class);
return dispatcher;
}
}

View File

@@ -2,11 +2,12 @@ package sonia.scm.api.v2.resources;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.apache.shiro.authc.credential.PasswordService;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -22,11 +23,17 @@ import java.net.URISyntaxException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher;
@SubjectAware(
username = "trillian",
password = "secret",
configuration = "classpath:sonia/scm/repository/shiro.ini"
)
public class MeResourceTest {
@@ -34,8 +41,7 @@ public class MeResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
private Dispatcher dispatcher;
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
@Mock
@@ -47,22 +53,28 @@ public class MeResourceTest {
private UserManager userManager;
@InjectMocks
private UserToUserDtoMapperImpl userToDtoMapper;
private MeToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
@Mock
private PasswordService passwordService;
private User originalUser;
@Before
public void prepareEnvironment() throws Exception {
initMocks(this);
createDummyUser("trillian");
originalUser = createDummyUser("trillian");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture());
userToDtoMapper.setResourceLinks(resourceLinks);
MeResource meResource = new MeResource(userToDtoMapper, userManager);
dispatcher.getRegistry().addSingletonResource(meResource);
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getUserTypeChecker()).thenCallRealMethod();
when(userManager.getDefaultType()).thenReturn("xml");
MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService);
when(uriInfo.getApiRestUri()).thenReturn(URI.create("/"));
when(scmPathInfoStore.get()).thenReturn(uriInfo);
dispatcher = createDispatcher(meResource);
}
@Test
@@ -76,14 +88,77 @@ public class MeResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"trillian\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/trillian\"}"));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/me/\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}"));
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String encryptedNewPassword = "encrypted123";
String oldPassword = "notEncriptedSecret";
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + MeResource.ME_PATH_V2 + "password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(eq(newPassword))).thenReturn(encryptedNewPassword);
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals(encryptedNewPassword, updatedUser.getPassword());
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
originalUser.setType("not an xml type");
String newPassword = "pwd123";
String oldPassword = "notEncriptedSecret";
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + MeResource.ME_PATH_V2 + "password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldGet400OnChangePasswordIfOldPasswordDoesNotMatchOriginalPassword() throws Exception {
String newPassword = "pwd123";
String oldPassword = "notEncriptedSecret";
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + MeResource.ME_PATH_V2 + "password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("differentThanSecret");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
private User createDummyUser(String name) {
User user = new User();
user.setName(name);
user.setType("xml");
user.setPassword("secret");
user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user);

View File

@@ -0,0 +1,135 @@
package sonia.scm.api.v2.resources;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class MeToUserDtoMapperTest {
private final URI baseUri = URI.create("http://example.com/base/");
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks
private MeToUserDtoMapperImpl mapper;
private final Subject subject = mock(Subject.class);
private final ThreadState subjectThreadState = new SubjectThreadState(subject);
private URI expectedBaseUri;
private URI expectedUserBaseUri;
@Before
public void init() {
initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(MeResource.ME_PATH_V2 + "/");
expectedUserBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind();
ThreadContext.bind(subject);
}
@After
public void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test
public void shouldMapTheUpdateLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no update link", userDto.getLinks().getLinkBy("update").isPresent());
}
@Test
public void shouldMapTheSelfLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.toString(), userDto.getLinks().getLinkBy("self").get().getHref());
}
@Test
public void shouldMapTheDeleteLink() {
User user = createDefaultUser();
when(subject.isPermitted("user:delete:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref());
when(subject.isPermitted("user:delete:abc")).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no delete link", userDto.getLinks().getLinkBy("delete").isPresent());
}
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).as("hide password for the me resource").isBlank();
}
private User createDefaultUser() {
User user = new User();
user.setName("abc");
user.setCreationDate(1L);
return user;
}
}

View File

@@ -12,7 +12,9 @@ public class ResourceLinksMock {
ScmPathInfo uriInfo = mock(ScmPathInfo.class);
when(uriInfo.getApiRestUri()).thenReturn(baseUri);
when(resourceLinks.user()).thenReturn(new ResourceLinks.UserLinks(uriInfo));
ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo);
when(resourceLinks.user()).thenReturn(userLinks);
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks));
when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo));
when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo));
when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo));

View File

@@ -23,18 +23,9 @@ public class UserDtoToUserMapperTest {
@Test
public void shouldMapFields() {
UserDto dto = createDefaultDto();
User user = mapper.map(dto, "original password");
User user = mapper.map(dto, "used password");
assertEquals("abc" , user.getName());
}
@Test
public void shouldEncodePassword() {
when(passwordService.encryptPassword("unencrypted")).thenReturn("encrypted");
UserDto dto = createDefaultDto();
dto.setPassword("unencrypted");
User user = mapper.map(dto, "original password");
assertEquals("encrypted" , user.getPassword());
assertEquals("used password" , user.getPassword());
}
@Before

View File

@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertEquals;
@@ -61,19 +62,23 @@ public class UserRootResourceTest {
private UserToUserDtoMapperImpl userToDtoMapper;
private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
private User originalUser;
@Before
public void prepareEnvironment() throws Exception {
initMocks(this);
User dummyUser = createDummyUser("Neo");
originalUser = createDummyUser("Neo");
when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]);
when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod();
when(userManager.getUserTypeChecker()).thenCallRealMethod();
doNothing().when(userManager).modify(userCaptor.capture());
doNothing().when(userManager).delete(userCaptor.capture());
when(userManager.getDefaultType()).thenReturn("xml");
UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks);
UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper,
userCollectionToDtoMapper, resourceLinks);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager);
userCollectionToDtoMapper, resourceLinks, passwordService);
UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService);
UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource),
Providers.of(userResource));
@@ -89,7 +94,6 @@ public class UserRootResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}"));
assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
}
@@ -104,13 +108,48 @@ public class UserRootResourceTest {
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertTrue(response.getContentAsString().contains("\"name\":\"Neo\""));
assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\""));
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}"));
assertFalse(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}"));
}
@Test
public void shouldCreateNewUserWithEncryptedPassword() throws Exception {
public void shouldEncryptPasswordBeforeChanging() throws Exception {
String newPassword = "pwd123";
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals("encrypted123", updatedUser.getPassword());
}
@Test
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
originalUser.setType("not an xml type");
String newPassword = "pwd123";
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
MockHttpRequest request = MockHttpRequest
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
.contentType(VndMediaType.PASSWORD_CHANGE)
.content(content.getBytes());
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
}
@Test
public void shouldEncryptPasswordBeforeCreatingUser() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/user-test-create.json");
byte[] userJson = Resources.toByteArray(url);
@@ -130,7 +169,7 @@ public class UserRootResourceTest {
}
@Test
public void shouldUpdateChangedUserWithEncryptedPassword() throws Exception {
public void shouldIgnoreGivenPasswordOnUpdatingUser() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/user-test-update.json");
byte[] userJson = Resources.toByteArray(url);
@@ -139,14 +178,13 @@ public class UserRootResourceTest {
.contentType(VndMediaType.USER)
.content(userJson);
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(userManager).modify(any(User.class));
User updatedUser = userCaptor.getValue();
assertEquals("encrypted123", updatedUser.getPassword());
assertEquals(originalUser.getPassword(), updatedUser.getPassword());
}
@Test
@@ -154,7 +192,7 @@ public class UserRootResourceTest {
MockHttpRequest request = MockHttpRequest
.post("/" + UserRootResource.USERS_PATH_V2)
.contentType(VndMediaType.USER)
.content(new byte[] {});
.content(new byte[]{});
MockHttpResponse response = new MockHttpResponse();
when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123");
@@ -265,6 +303,7 @@ public class UserRootResourceTest {
private User createDummyUser(String name) {
User user = new User();
user.setName(name);
user.setType("xml");
user.setPassword("redpill");
user.setCreationDate(System.currentTimeMillis());
when(userManager.get(name)).thenReturn(user);

View File

@@ -8,14 +8,17 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import sonia.scm.api.rest.resources.UserResource;
import org.mockito.Mock;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import java.net.URI;
import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -26,6 +29,9 @@ public class UserToUserDtoMapperTest {
@SuppressWarnings("unused") // Is injected
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private UserManager userManager;
@InjectMocks
private UserToUserDtoMapperImpl mapper;
@@ -37,6 +43,7 @@ public class UserToUserDtoMapperTest {
@Before
public void init() {
initMocks(this);
when(userManager.getDefaultType()).thenReturn("xml");
expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/");
subjectThreadState.bind();
ThreadContext.bind(subject);
@@ -53,11 +60,42 @@ public class UserToUserDtoMapperTest {
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref());
assertEquals("expected update link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref());
}
@Test
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
User user = createDefaultUser();
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
UserDto userDto = mapper.map(user);
assertEquals("expected password link with modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(subject.isPermitted("user:modify:abc")).thenReturn(false);
userDto = mapper.map(user);
assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref());
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
userDto = mapper.map(user);
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
}
@Test
public void shouldGetEmptyPasswordProperty() {
User user = createDefaultUser();
user.setPassword("myHighSecurePassword");
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
UserDto userDto = mapper.map(user);
assertThat(userDto.getPassword()).isBlank();
}
@Test
public void shouldMapLinks_forDelete() {
User user = createDefaultUser();
@@ -97,16 +135,6 @@ public class UserToUserDtoMapperTest {
assertEquals("abc", userDto.getName());
}
@Test
public void shouldRemovePassword() {
User user = createDefaultUser();
user.setPassword("password");
UserDto userDto = mapper.map(user);
assertEquals(UserResource.DUMMY_PASSWORT, userDto.getPassword());
}
@Test
public void shouldMapTimes() {
User user = createDefaultUser();