mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-04 20:45:52 +01:00
merge with 2.0.0-m3
This commit is contained in:
@@ -71,7 +71,7 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(T object) throws NotFoundException {
|
||||
public void delete(T object){
|
||||
decorated.delete(object);
|
||||
}
|
||||
|
||||
@@ -82,12 +82,12 @@ public class ManagerDecorator<T extends ModelObject> implements Manager<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(T object) throws NotFoundException {
|
||||
public void modify(T object){
|
||||
decorated.modify(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh(T object) throws NotFoundException {
|
||||
public void refresh(T object){
|
||||
decorated.refresh(object);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package sonia.scm;
|
||||
|
||||
public class NotFoundException extends Exception {
|
||||
public class NotFoundException extends RuntimeException {
|
||||
public NotFoundException(String type, String id) {
|
||||
super(type + " with id '" + id + "' not found");
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ 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 static final String WRONG_USER_TYPE = "User of type %s are not allowed to change password";
|
||||
|
||||
public ChangePasswordNotAllowedException(String message) {
|
||||
super(message);
|
||||
public ChangePasswordNotAllowedException(String type) {
|
||||
super(String.format(WRONG_USER_TYPE, type));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ 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);
|
||||
public InvalidPasswordException() {
|
||||
super("The given Password does not match with the stored one.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ import java.security.Principal;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@StaticPermissions(value = "user", globalPermissions = {"create", "list", "autocomplete"})
|
||||
@StaticPermissions(
|
||||
value = "user",
|
||||
globalPermissions = {"create", "list", "autocomplete"},
|
||||
permissions = {"read", "modify", "delete", "changePassword"})
|
||||
@XmlRootElement(name = "users")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject, ReducedModelObject
|
||||
@@ -274,10 +277,6 @@ public class User extends BasicPropertiesAware implements Principal, ModelObject
|
||||
//J+
|
||||
}
|
||||
|
||||
public User changePassword(String password){
|
||||
setPassword(password);
|
||||
return this;
|
||||
}
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,11 +38,7 @@ package sonia.scm.user;
|
||||
import sonia.scm.Manager;
|
||||
import sonia.scm.search.Searchable;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE;
|
||||
|
||||
/**
|
||||
* The central class for managing {@link User} objects.
|
||||
@@ -75,18 +71,6 @@ public interface UserManager
|
||||
*/
|
||||
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());
|
||||
}
|
||||
@@ -99,5 +83,17 @@ public interface UserManager
|
||||
*/
|
||||
Collection<User> autocomplete(String filter);
|
||||
|
||||
/**
|
||||
* Changes the password of the logged in user.
|
||||
* @param oldPassword The current encrypted password of the user.
|
||||
* @param newPassword The new encrypted password of the user.
|
||||
*/
|
||||
void changePasswordForLoggedInUser(String oldPassword, String newPassword);
|
||||
|
||||
/**
|
||||
* Overwrites the password for the given user id. This needs user write privileges.
|
||||
* @param userId The id of the user to change the password for.
|
||||
* @param newPassword The new encrypted password.
|
||||
*/
|
||||
void overwritePassword(String userId, String newPassword);
|
||||
}
|
||||
|
||||
@@ -126,6 +126,15 @@ public class UserManagerDecorator extends ManagerDecorator<User>
|
||||
return decorated.autocomplete(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
|
||||
decorated.changePasswordForLoggedInUser(oldPassword, newPassword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwritePassword(String userId, String newPassword) {
|
||||
decorated.overwritePassword(userId, newPassword);
|
||||
}
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
|
||||
@@ -39,6 +39,8 @@ public class VndMediaType {
|
||||
public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX;
|
||||
@SuppressWarnings("squid:S2068")
|
||||
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
|
||||
@SuppressWarnings("squid:S2068")
|
||||
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
|
||||
|
||||
public static final String ME = PREFIX + "me" + SUFFIX;
|
||||
public static final String SOURCE = PREFIX + "source" + SUFFIX;
|
||||
|
||||
@@ -154,7 +154,7 @@ public class SyncingRealmHelperTest {
|
||||
* Tests {@link SyncingRealmHelper#store(Group)} with an existing group.
|
||||
*/
|
||||
@Test
|
||||
public void testStoreGroupModify() throws NotFoundException {
|
||||
public void testStoreGroupModify(){
|
||||
Group group = new Group("unit-test", "heartOfGold");
|
||||
|
||||
when(groupManager.get("heartOfGold")).thenReturn(group);
|
||||
@@ -191,7 +191,7 @@ public class SyncingRealmHelperTest {
|
||||
* Tests {@link SyncingRealmHelper#store(User)} with an existing user.
|
||||
*/
|
||||
@Test
|
||||
public void testStoreUserModify() throws NotFoundException {
|
||||
public void testStoreUserModify(){
|
||||
when(userManager.contains("tricia")).thenReturn(Boolean.TRUE);
|
||||
|
||||
User user = new User("tricia");
|
||||
|
||||
@@ -38,6 +38,30 @@ public class MeITCase {
|
||||
.assertStatusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminUserShouldChangeOwnPassword() {
|
||||
String newPassword = "pass1";
|
||||
String username = "user1";
|
||||
String password = "pass";
|
||||
TestData.createUser(username, password,false,"xml", "em@l.de");
|
||||
// user change the own password
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(username, password)
|
||||
.requestMe()
|
||||
.assertStatusCode(200)
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.FALSE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.assertType(s -> assertThat(s).isEqualTo("xml"))
|
||||
.requestChangePassword(password, newPassword)
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password than undo changes
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(username, newPassword)
|
||||
.requestMe()
|
||||
.assertStatusCode(200);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||
String newUser = "user";
|
||||
|
||||
@@ -29,7 +29,7 @@ public class UserITCase {
|
||||
.assertStatusCode(200)
|
||||
.assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))
|
||||
.assertPassword(Assert::assertNull)
|
||||
.requestChangePassword(newPassword) // the oldPassword is not needed in the user resource
|
||||
.requestChangePassword(newPassword)
|
||||
.assertStatusCode(204);
|
||||
// assert password is changed -> login with the new Password
|
||||
ScmRequests.start()
|
||||
@@ -65,6 +65,25 @@ public class UserITCase {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAdminUserShouldNotChangePasswordOfOtherUser() {
|
||||
String user = "user";
|
||||
String password = "pass";
|
||||
TestData.createUser(user, password, false, "xml", "em@l.de");
|
||||
String user2 = "user2";
|
||||
TestData.createUser(user2, password, false, "xml", "em@l.de");
|
||||
ScmRequests.start()
|
||||
.requestIndexResource(user, password)
|
||||
.assertUsersLinkDoesNotExists();
|
||||
// use the users/ endpoint bypassed the index resource
|
||||
ScmRequests.start()
|
||||
.requestUser(user, password, user2)
|
||||
.assertStatusCode(403);
|
||||
// use the users/password endpoint bypassed the index and users resources
|
||||
ScmRequests.start()
|
||||
.requestUserChangePassword(user, password, user2, "newPassword")
|
||||
.assertStatusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHidePasswordLinkIfUserTypeIsNotXML() {
|
||||
|
||||
@@ -2,6 +2,9 @@ package sonia.scm.it.utils;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.response.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,7 +27,8 @@ import static sonia.scm.it.utils.TestData.createPasswordChangeJson;
|
||||
*/
|
||||
public class ScmRequests {
|
||||
|
||||
private String url;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScmRequests.class);
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
@@ -38,6 +42,18 @@ public class ScmRequests {
|
||||
return new IndexResponse(applyGETRequest(RestUtil.REST_BASE_URL.toString()));
|
||||
}
|
||||
|
||||
public <SELF extends UserResponse<SELF, T>, T extends ModelResponse> UserResponse<SELF,T> requestUser(String username, String password, String pathParam) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new UserResponse<>(applyGETRequest(RestUtil.REST_BASE_URL.resolve("users/"+pathParam).toString()), null);
|
||||
}
|
||||
|
||||
public ChangePasswordResponse<ChangePasswordResponse> requestUserChangePassword(String username, String password, String userPathParam, String newPassword) {
|
||||
setUsername(username);
|
||||
setPassword(password);
|
||||
return new ChangePasswordResponse<>(applyPUTRequest(RestUtil.REST_BASE_URL.resolve("users/"+userPathParam+"/password").toString(), VndMediaType.PASSWORD_OVERWRITE, TestData.createPasswordChangeJson(password,newPassword)), null);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a GET Request to the extracted url from the given link
|
||||
@@ -73,6 +89,7 @@ public class ScmRequests {
|
||||
* @return the response of the GET request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyGETRequestWithQueryParams(String url, String params) {
|
||||
LOG.info("GET {}", url);
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
@@ -115,6 +132,7 @@ public class ScmRequests {
|
||||
* @return the response of the PUT request using the given <code>url</code>
|
||||
*/
|
||||
private Response applyPUTRequest(String url, String mediaType, String body) {
|
||||
LOG.info("PUT {}", url);
|
||||
return RestAssured.given()
|
||||
.auth().preemptive().basic(username, password)
|
||||
.when()
|
||||
@@ -132,7 +150,6 @@ public class ScmRequests {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
|
||||
public class IndexResponse extends ModelResponse<IndexResponse, IndexResponse> {
|
||||
public static final String LINK_AUTOCOMPLETE_USERS = "_links.autocomplete.find{it.name=='users'}.href";
|
||||
public static final String LINK_AUTOCOMPLETE_GROUPS = "_links.autocomplete.find{it.name=='groups'}.href";
|
||||
@@ -160,10 +177,15 @@ public class ScmRequests {
|
||||
return new MeResponse<>(applyGETRequestFromLink(response, LINK_ME), this);
|
||||
}
|
||||
|
||||
public UserResponse<IndexResponse> requestUser(String username) {
|
||||
public UserResponse<? extends UserResponse, IndexResponse> requestUser(String username) {
|
||||
return new UserResponse<>(applyGETRequestFromLinkWithParams(response, LINK_USERS, username), this);
|
||||
}
|
||||
|
||||
public IndexResponse assertUsersLinkDoesNotExists() {
|
||||
return super.assertPropertyPathDoesNotExists(LINK_USERS);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class RepositoryResponse<PREV extends ModelResponse> extends ModelResponse<RepositoryResponse<PREV>, PREV> {
|
||||
@@ -267,17 +289,19 @@ public class ScmRequests {
|
||||
|
||||
}
|
||||
|
||||
public class MeResponse<PREV extends ModelResponse> extends UserResponse<PREV> {
|
||||
public class MeResponse<PREV extends ModelResponse> extends UserResponse<MeResponse<PREV>, PREV> {
|
||||
|
||||
|
||||
public MeResponse(Response response, PREV previousResponse) {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
|
||||
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
|
||||
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
|
||||
}
|
||||
}
|
||||
|
||||
public class UserResponse<PREV extends ModelResponse> extends ModelResponse<UserResponse<PREV>, PREV> {
|
||||
public class UserResponse<SELF extends UserResponse<SELF, PREV>, PREV extends ModelResponse> extends ModelResponse<SELF, PREV> {
|
||||
|
||||
public static final String LINKS_PASSWORD_HREF = "_links.password.href";
|
||||
|
||||
@@ -285,34 +309,29 @@ public class ScmRequests {
|
||||
super(response, previousResponse);
|
||||
}
|
||||
|
||||
public UserResponse<PREV> assertPassword(Consumer<String> assertPassword) {
|
||||
public SELF assertPassword(Consumer<String> assertPassword) {
|
||||
return super.assertSingleProperty(assertPassword, "password");
|
||||
}
|
||||
|
||||
public UserResponse<PREV> assertType(Consumer<String> assertType) {
|
||||
public SELF assertType(Consumer<String> assertType) {
|
||||
return assertSingleProperty(assertType, "type");
|
||||
}
|
||||
|
||||
public UserResponse<PREV> assertAdmin(Consumer<Boolean> assertAdmin) {
|
||||
public SELF assertAdmin(Consumer<Boolean> assertAdmin) {
|
||||
return assertSingleProperty(assertAdmin, "admin");
|
||||
}
|
||||
|
||||
public UserResponse<PREV> assertPasswordLinkDoesNotExists() {
|
||||
public SELF assertPasswordLinkDoesNotExists() {
|
||||
return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF);
|
||||
}
|
||||
|
||||
public UserResponse<PREV> assertPasswordLinkExists() {
|
||||
public SELF assertPasswordLinkExists() {
|
||||
return assertPropertyPathExists(LINKS_PASSWORD_HREF);
|
||||
}
|
||||
|
||||
public ChangePasswordResponse<UserResponse> requestChangePassword(String newPassword) {
|
||||
return requestChangePassword(null, newPassword);
|
||||
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_OVERWRITE, createPasswordChangeJson(null, newPassword)), this);
|
||||
}
|
||||
|
||||
public ChangePasswordResponse<UserResponse> requestChangePassword(String oldPassword, String newPassword) {
|
||||
return new ChangePasswordResponse<>(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword)), this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,9 +707,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -726,7 +726,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,9 +641,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -660,7 +660,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"@scm-manager/ui-extensions": "^0.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,9 +641,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -660,7 +660,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -196,7 +196,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> {
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void testModifyNotExisting() throws NotFoundException, ConcurrentModificationException {
|
||||
public void testModifyNotExisting() {
|
||||
manager.modify(UserTestData.createZaphod());
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ public abstract class UserManagerTestBase extends ManagerTestBase<User> {
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void testRefreshNotFound() throws NotFoundException {
|
||||
public void testRefreshNotFound(){
|
||||
manager.refresh(UserTestData.createDent());
|
||||
}
|
||||
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
"eslint-fix": "eslint src --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15",
|
||||
"@scm-manager/ui-bundler": "^0.0.17",
|
||||
"create-index": "^2.3.0",
|
||||
"enzyme": "^3.5.0",
|
||||
"enzyme-adapter-react-16": "^1.3.1",
|
||||
"flow-bin": "^0.79.1",
|
||||
"flow-typed": "^2.5.1",
|
||||
"jest": "^23.5.0",
|
||||
"raf": "^3.4.0"
|
||||
"raf": "^3.4.0",
|
||||
"react-router-enzyme-context": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"moment": "^2.22.2",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-i18next": "^7.11.0",
|
||||
"react-jss": "^8.6.1",
|
||||
"react-router-dom": "^4.3.1",
|
||||
|
||||
133
scm-ui-components/packages/ui-components/src/LinkPaginator.js
Normal file
133
scm-ui-components/packages/ui-components/src/LinkPaginator.js
Normal file
@@ -0,0 +1,133 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import {translate} from "react-i18next";
|
||||
import type {PagedCollection} from "@scm-manager/ui-types";
|
||||
import {Button} from "./buttons";
|
||||
|
||||
type Props = {
|
||||
collection: PagedCollection,
|
||||
page: number,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class LinkPaginator extends React.Component<Props> {
|
||||
|
||||
renderFirstButton() {
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-link"}
|
||||
label={"1"}
|
||||
disabled={false}
|
||||
link={"1"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderPreviousButton(label?: string) {
|
||||
const { page } = this.props;
|
||||
const previousPage = page - 1;
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-previous"}
|
||||
label={label ? label : previousPage.toString()}
|
||||
disabled={!this.hasLink("prev")}
|
||||
link={`${previousPage}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
hasLink(name: string) {
|
||||
const { collection } = this.props;
|
||||
return collection._links[name];
|
||||
}
|
||||
|
||||
renderNextButton(label?: string) {
|
||||
const { page } = this.props;
|
||||
const nextPage = page + 1;
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-next"}
|
||||
label={label ? label : nextPage.toString()}
|
||||
disabled={!this.hasLink("next")}
|
||||
link={`${nextPage}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLastButton() {
|
||||
const { collection } = this.props;
|
||||
return (
|
||||
<Button
|
||||
className={"pagination-link"}
|
||||
label={`${collection.pageTotal}`}
|
||||
disabled={false}
|
||||
link={`${collection.pageTotal}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
separator() {
|
||||
return <span className="pagination-ellipsis">…</span>;
|
||||
}
|
||||
|
||||
currentPage(page: number) {
|
||||
return (
|
||||
<Button
|
||||
className="pagination-link is-current"
|
||||
label={page}
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
pageLinks() {
|
||||
const { collection } = this.props;
|
||||
|
||||
const links = [];
|
||||
const page = collection.page + 1;
|
||||
const pageTotal = collection.pageTotal;
|
||||
if (page > 1) {
|
||||
links.push(this.renderFirstButton());
|
||||
}
|
||||
if (page > 3) {
|
||||
links.push(this.separator());
|
||||
}
|
||||
if (page > 2) {
|
||||
links.push(this.renderPreviousButton());
|
||||
}
|
||||
|
||||
links.push(this.currentPage(page));
|
||||
|
||||
if (page + 1 < pageTotal) {
|
||||
links.push(this.renderNextButton());
|
||||
}
|
||||
if (page + 2 < pageTotal)
|
||||
//if there exists pages between next and last
|
||||
links.push(this.separator());
|
||||
if (page < pageTotal) {
|
||||
links.push(this.renderLastButton());
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<nav className="pagination is-centered" aria-label="pagination">
|
||||
{this.renderPreviousButton(t("paginator.previous"))}
|
||||
<ul className="pagination-list">
|
||||
{this.pageLinks().map((link, index) => {
|
||||
return <li key={index}>{link}</li>;
|
||||
})}
|
||||
</ul>
|
||||
{this.renderNextButton(t("paginator.next"))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("commons")(LinkPaginator);
|
||||
@@ -18,8 +18,10 @@ class Paginator extends React.Component<Props> {
|
||||
createAction = (linkType: string) => () => {
|
||||
const { collection, onPageChange } = this.props;
|
||||
if (onPageChange) {
|
||||
const link = collection._links[linkType].href;
|
||||
onPageChange(link);
|
||||
const link = collection._links[linkType];
|
||||
if (link && link.href) {
|
||||
onPageChange(link.href);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,13 @@ import React from "react";
|
||||
import { mount, shallow } from "enzyme";
|
||||
import "./tests/enzyme";
|
||||
import "./tests/i18n";
|
||||
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import Paginator from "./Paginator";
|
||||
|
||||
describe("paginator rendering tests", () => {
|
||||
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
const dummyLink = {
|
||||
href: "https://dummy"
|
||||
};
|
||||
@@ -18,7 +21,10 @@ describe("paginator rendering tests", () => {
|
||||
_links: {}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(7);
|
||||
for (let button of buttons) {
|
||||
@@ -37,7 +43,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(5);
|
||||
|
||||
@@ -73,7 +82,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(6);
|
||||
|
||||
@@ -112,7 +124,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(5);
|
||||
|
||||
@@ -148,7 +163,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(6);
|
||||
|
||||
@@ -189,7 +207,10 @@ describe("paginator rendering tests", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const paginator = shallow(<Paginator collection={collection} />);
|
||||
const paginator = shallow(
|
||||
<Paginator collection={collection} />,
|
||||
options.get()
|
||||
);
|
||||
const buttons = paginator.find("Button");
|
||||
expect(buttons.length).toBe(7);
|
||||
|
||||
@@ -244,7 +265,8 @@ describe("paginator rendering tests", () => {
|
||||
};
|
||||
|
||||
const paginator = mount(
|
||||
<Paginator collection={collection} onPageChange={callMe} />
|
||||
<Paginator collection={collection} onPageChange={callMe} />,
|
||||
options.get()
|
||||
);
|
||||
paginator.find("Button.pagination-previous").simulate("click");
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Link } from "react-router-dom";
|
||||
import { withRouter } from "react-router-dom";
|
||||
|
||||
export type ButtonProps = {
|
||||
label: string,
|
||||
@@ -16,7 +16,10 @@ export type ButtonProps = {
|
||||
|
||||
type Props = ButtonProps & {
|
||||
type: string,
|
||||
color: string
|
||||
color: string,
|
||||
|
||||
// context prop
|
||||
history: any
|
||||
};
|
||||
|
||||
class Button extends React.Component<Props> {
|
||||
@@ -25,14 +28,22 @@ class Button extends React.Component<Props> {
|
||||
color: "default"
|
||||
};
|
||||
|
||||
renderButton = () => {
|
||||
onClick = (event: Event) => {
|
||||
const { action, link, history } = this.props;
|
||||
if (action) {
|
||||
action(event);
|
||||
} else if (link) {
|
||||
history.push(link);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
loading,
|
||||
disabled,
|
||||
type,
|
||||
color,
|
||||
action,
|
||||
fullWidth,
|
||||
className
|
||||
} = this.props;
|
||||
@@ -42,7 +53,7 @@ class Button extends React.Component<Props> {
|
||||
<button
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
onClick={action ? action : (event: Event) => {}}
|
||||
onClick={this.onClick}
|
||||
className={classNames(
|
||||
"button",
|
||||
"is-" + color,
|
||||
@@ -56,14 +67,6 @@ class Button extends React.Component<Props> {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { link } = this.props;
|
||||
if (link) {
|
||||
return <Link to={link}>{this.renderButton()}</Link>;
|
||||
} else {
|
||||
return this.renderButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
export default withRouter(Button);
|
||||
|
||||
@@ -15,9 +15,11 @@ export { default as Logo } from "./Logo.js";
|
||||
export { default as MailLink } from "./MailLink.js";
|
||||
export { default as Notification } from "./Notification.js";
|
||||
export { default as Paginator } from "./Paginator.js";
|
||||
export { default as LinkPaginator } from "./LinkPaginator.js";
|
||||
export { default as ProtectedRoute } from "./ProtectedRoute.js";
|
||||
export { default as Help } from "./Help.js";
|
||||
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
|
||||
export { getPageFromMatch } from "./urls";
|
||||
|
||||
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js";
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ type Props = {
|
||||
to: string,
|
||||
label: string,
|
||||
activeOnlyWhenExact?: boolean,
|
||||
otherLocation: (route: any) => boolean
|
||||
activeWhenMatch?: (route: any) => boolean
|
||||
};
|
||||
|
||||
class NavLink extends React.Component<Props> {
|
||||
@@ -16,11 +16,17 @@ class NavLink extends React.Component<Props> {
|
||||
activeOnlyWhenExact: true
|
||||
};
|
||||
|
||||
|
||||
isActive(route: any) {
|
||||
const { activeWhenMatch } = this.props;
|
||||
return route.match || (activeWhenMatch && activeWhenMatch(route));
|
||||
}
|
||||
|
||||
renderLink = (route: any) => {
|
||||
const { to, label, otherLocation } = this.props;
|
||||
const { to, label } = this.props;
|
||||
return (
|
||||
<li>
|
||||
<Link className={route.match || (otherLocation && otherLocation(route)) ? "is-active" : ""} to={to}>
|
||||
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
import { getProtocolLinkByType, getTypePredicate } from "./repositories";
|
||||
import { getProtocolLinkByType } from "./repositories";
|
||||
|
||||
describe("getProtocolLinkByType tests", () => {
|
||||
|
||||
|
||||
@@ -4,3 +4,11 @@ export const contextPath = window.ctxPath || "";
|
||||
export function withContextPath(path: string) {
|
||||
return contextPath + path;
|
||||
}
|
||||
|
||||
export function getPageFromMatch(match: any) {
|
||||
let page = parseInt(match.params.page, 10);
|
||||
if (isNaN(page) || !page) {
|
||||
page = 1;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
27
scm-ui-components/packages/ui-components/src/urls.test.js
Normal file
27
scm-ui-components/packages/ui-components/src/urls.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @flow
|
||||
import { getPageFromMatch } from "./urls";
|
||||
|
||||
describe("tests for getPageFromMatch", () => {
|
||||
function createMatch(page: string) {
|
||||
return {
|
||||
params: {
|
||||
page
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
it("should return 1 for NaN", () => {
|
||||
const match = createMatch("any");
|
||||
expect(getPageFromMatch(match)).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 for 0", () => {
|
||||
const match = createMatch("0");
|
||||
expect(getPageFromMatch(match)).toBe(1);
|
||||
});
|
||||
|
||||
it("should return the given number", () => {
|
||||
const match = createMatch("42");
|
||||
expect(getPageFromMatch(match)).toBe(42);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
8011
scm-ui-components/packages/ui-components/yarn.lock
Normal file
8011
scm-ui-components/packages/ui-components/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
||||
"check": "flow check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15"
|
||||
"@scm-manager/ui-bundler": "^0.0.17"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
//@flow
|
||||
import type {Links} from "./hal";
|
||||
import type {Tag} from "./Tags";
|
||||
import type {Branch} from "./Branch";
|
||||
import type {Branch} from "./Branches";
|
||||
|
||||
export type Changeset = {
|
||||
id: string,
|
||||
date: Date,
|
||||
author: {
|
||||
name: string,
|
||||
mail: string
|
||||
mail?: string
|
||||
},
|
||||
description: string,
|
||||
_links: Links,
|
||||
_embedded: {
|
||||
tags: Tag[],
|
||||
branches: Branch[],
|
||||
parents: ParentChangeset[]
|
||||
tags?: Tag[],
|
||||
branches?: Branch[],
|
||||
parents?: ParentChangeset[]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@ export type Link = {
|
||||
name?: string
|
||||
};
|
||||
|
||||
export type Links = { [string]: Link | Link[] };
|
||||
type LinkValue = Link | Link[];
|
||||
|
||||
// TODO use LinkValue
|
||||
export type Links = { [string]: any };
|
||||
|
||||
export type Collection = {
|
||||
_embedded: Object,
|
||||
// $FlowFixMe
|
||||
_links: Links
|
||||
};
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ export type { Group, Member } from "./Group";
|
||||
export type { Repository, RepositoryCollection, RepositoryGroup } from "./Repositories";
|
||||
export type { RepositoryType, RepositoryTypeCollection } from "./RepositoryTypes";
|
||||
|
||||
export type { Branch } from "./Branches";
|
||||
|
||||
export type { Changeset } from "./Changesets";
|
||||
|
||||
export type { Tag } from "./Tags"
|
||||
export type { Tag } from "./Tags";
|
||||
|
||||
export type { Config } from "./Config";
|
||||
|
||||
|
||||
@@ -707,9 +707,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -726,7 +726,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
"i18next-browser-languagedetector": "^2.2.2",
|
||||
"i18next-fetch-backend": "^0.1.0",
|
||||
"moment": "^2.22.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"react": "^16.4.2",
|
||||
"react-dom": "^16.4.2",
|
||||
"react": "^16.5.2",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-i18next": "^7.9.0",
|
||||
"react-jss": "^8.6.0",
|
||||
"react-redux": "^5.0.7",
|
||||
@@ -44,13 +43,14 @@
|
||||
"pre-commit": "jest && flow && eslint src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scm-manager/ui-bundler": "^0.0.15",
|
||||
"@scm-manager/ui-bundler": "^0.0.17",
|
||||
"copyfiles": "^2.0.0",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"fetch-mock": "^6.5.0",
|
||||
"flow-typed": "^2.5.1",
|
||||
"jest": "^23.5.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"node-sass-chokidar": "^1.3.0",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"prettier": "^1.13.7",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"changeset": {
|
||||
"id": "ID",
|
||||
"description": "Description",
|
||||
"contact": "Contact",
|
||||
"date": "Date",
|
||||
"summary": "Changeset {{id}} committed {{time}}"
|
||||
},
|
||||
"author": {
|
||||
"name": "Author",
|
||||
"mail": "Mail"
|
||||
},
|
||||
"changeset-error": {
|
||||
"title": "Error",
|
||||
"subtitle": "Unknown changeset error"
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@
|
||||
"actions-label": "Actions",
|
||||
"back-label": "Back",
|
||||
"navigation-label": "Navigation",
|
||||
"history": "Commits",
|
||||
"information": "Information",
|
||||
"history": "History",
|
||||
"permissions": "Permissions"
|
||||
},
|
||||
"create": {
|
||||
@@ -45,6 +45,24 @@
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"changesets": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Could not fetch changesets",
|
||||
"changeset": {
|
||||
"id": "ID",
|
||||
"description": "Description",
|
||||
"contact": "Contact",
|
||||
"date": "Date",
|
||||
"summary": "Changeset {{id}} committed {{time}}"
|
||||
},
|
||||
"author": {
|
||||
"name": "Author",
|
||||
"mail": "Mail"
|
||||
}
|
||||
},
|
||||
"branch-selector": {
|
||||
"label": "Branches"
|
||||
},
|
||||
"permission": {
|
||||
"error-title": "Error",
|
||||
"error-subtitle": "Unknown permissions error",
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
import { Route, Redirect, withRouter } from "react-router";
|
||||
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
|
||||
|
||||
import Overview from "../repos/containers/Overview";
|
||||
import Users from "../users/containers/Users";
|
||||
import Login from "../containers/Login";
|
||||
import Logout from "../containers/Logout";
|
||||
|
||||
import { Switch } from "react-router-dom";
|
||||
import { ProtectedRoute } from "@scm-manager/ui-components";
|
||||
import AddUser from "../users/containers/AddUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
options: string[],
|
||||
optionSelected: string => void,
|
||||
preselectedOption: string
|
||||
}
|
||||
preselectedOption?: string,
|
||||
className: any
|
||||
};
|
||||
|
||||
class DropDown extends React.Component<Props> {
|
||||
render() {
|
||||
const {options, preselectedOption} = this.props;
|
||||
return <div className="select">
|
||||
<select value={preselectedOption} onChange={this.change}>
|
||||
<option key=""></option>
|
||||
const { options, preselectedOption, className } = this.props;
|
||||
return (
|
||||
<div className={classNames(className, "select")}>
|
||||
<select
|
||||
value={preselectedOption ? preselectedOption : ""}
|
||||
onChange={this.change}
|
||||
>
|
||||
<option key="" />
|
||||
{options.map(option => {
|
||||
return <option key={option}
|
||||
value={option}>{option}</option>
|
||||
return (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
change = (event) => {
|
||||
change = (event: SyntheticInputEvent<HTMLSelectElement>) => {
|
||||
this.props.optionSelected(event.target.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default DropDown;
|
||||
|
||||
@@ -4,7 +4,6 @@ import "../../tests/enzyme";
|
||||
import "../../tests/i18n";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
import PermissionsNavLink from "./PermissionsNavLink";
|
||||
import EditNavLink from "./EditNavLink";
|
||||
|
||||
describe("PermissionsNavLink", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
@@ -10,18 +10,28 @@ type Props = {
|
||||
export default class ChangesetAuthor extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
if (!changeset.author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { name } = changeset.author;
|
||||
return (
|
||||
<>
|
||||
{changeset.author.name}{" "}
|
||||
<a
|
||||
className="is-hidden-mobile"
|
||||
href={"mailto:" + changeset.author.mail}
|
||||
>
|
||||
<
|
||||
{changeset.author.mail}
|
||||
>
|
||||
</a>
|
||||
{name} {this.renderMail()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderMail() {
|
||||
const { mail } = this.props.changeset.author;
|
||||
if (mail) {
|
||||
return (
|
||||
<a className="is-hidden-mobile" href={"mailto:" + mail}>
|
||||
<
|
||||
{mail}
|
||||
>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
scm-ui/src/repos/components/changesets/ChangesetAvatar.js
Normal file
30
scm-ui/src/repos/components/changesets/ChangesetAvatar.js
Normal file
@@ -0,0 +1,30 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import type { Changeset } from "@scm-manager/ui-types";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset
|
||||
};
|
||||
|
||||
class ChangesetAvatar extends React.Component<Props> {
|
||||
render() {
|
||||
const { changeset } = this.props;
|
||||
return (
|
||||
<ExtensionPoint
|
||||
name="repos.changeset-table.information"
|
||||
renderAll={true}
|
||||
props={{ changeset }}
|
||||
>
|
||||
{/* extension should render something like this: */}
|
||||
{/* <div className="image is-64x64"> */}
|
||||
{/* <figure className="media-left"> */}
|
||||
{/* <Image src="/some/image.jpg" alt="Logo" /> */}
|
||||
{/* </figure> */}
|
||||
{/* </div> */}
|
||||
</ExtensionPoint>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangesetAvatar;
|
||||
@@ -12,8 +12,14 @@ type Props = {
|
||||
class ChangesetList extends React.Component<Props> {
|
||||
render() {
|
||||
const { repository, changesets } = this.props;
|
||||
const content = changesets.map((changeset, index) => {
|
||||
return <ChangesetRow key={index} repository={repository} changeset={changeset} />;
|
||||
const content = changesets.map(changeset => {
|
||||
return (
|
||||
<ChangesetRow
|
||||
key={changeset.id}
|
||||
repository={repository}
|
||||
changeset={changeset}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return <div className={classNames("box")}>{content}</div>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Changeset, Repository } from "@scm-manager/ui-types";
|
||||
import type { Changeset, Repository, Tag } from "@scm-manager/ui-types";
|
||||
import classNames from "classnames";
|
||||
import { translate, Interpolate } from "react-i18next";
|
||||
import ChangesetAvatar from "./ChangesetAvatar";
|
||||
@@ -8,6 +8,8 @@ import ChangesetId from "./ChangesetId";
|
||||
import injectSheet from "react-jss";
|
||||
import { DateFromNow } from "@scm-manager/ui-components";
|
||||
import ChangesetAuthor from "./ChangesetAuthor";
|
||||
import ChangesetTag from "./ChangesetTag";
|
||||
import { compose } from "redux";
|
||||
|
||||
const styles = {
|
||||
pointer: {
|
||||
@@ -34,33 +36,55 @@ class ChangesetRow extends React.Component<Props> {
|
||||
return <ChangesetId changeset={changeset} repository={repository} />;
|
||||
};
|
||||
|
||||
getTags = () => {
|
||||
const { changeset } = this.props;
|
||||
return changeset._embedded.tags || [];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { changeset, classes } = this.props;
|
||||
const changesetLink = this.createLink(changeset);
|
||||
const dateFromNow = <DateFromNow date={changeset.date} />;
|
||||
const authorLine = <ChangesetAuthor changeset={changeset} />;
|
||||
|
||||
return (
|
||||
<article className={classNames("media", classes.inner)}>
|
||||
<figure className="media-left">
|
||||
<ChangesetAvatar changeset={changeset} />
|
||||
</figure>
|
||||
<div className={classNames("media-content", classes.withOverflow)}>
|
||||
<div className="content">
|
||||
<p className="is-ellipsis-overflow">
|
||||
{changeset.description}
|
||||
<br />
|
||||
<Interpolate
|
||||
i18nKey="changeset.summary"
|
||||
i18nKey="changesets.changeset.summary"
|
||||
id={changesetLink}
|
||||
time={dateFromNow}
|
||||
/>
|
||||
</p>{" "}
|
||||
<p className="is-size-7">{authorLine}</p>
|
||||
<div className="is-size-7">{authorLine}</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderTags()}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
renderTags = () => {
|
||||
const tags = this.getTags();
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<div className="media-right">
|
||||
{tags.map((tag: Tag) => {
|
||||
return <ChangesetTag key={tag.name} tag={tag} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(translate("changesets")(ChangesetRow));
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("repos")
|
||||
)(ChangesetRow);
|
||||
32
scm-ui/src/repos/components/changesets/ChangesetTag.js
Normal file
32
scm-ui/src/repos/components/changesets/ChangesetTag.js
Normal file
@@ -0,0 +1,32 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import type { Tag } from "@scm-manager/ui-types";
|
||||
import injectSheet from "react-jss";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
spacing: {
|
||||
marginRight: "4px"
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tag: Tag,
|
||||
|
||||
// context props
|
||||
classes: Object
|
||||
};
|
||||
|
||||
class ChangesetTag extends React.Component<Props> {
|
||||
render() {
|
||||
const { tag, classes } = this.props;
|
||||
return (
|
||||
<span className="tag is-info">
|
||||
<span className={classNames("fa", "fa-tag", classes.spacing)} />{" "}
|
||||
{tag.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectSheet(styles)(ChangesetTag);
|
||||
139
scm-ui/src/repos/containers/BranchRoot.js
Normal file
139
scm-ui/src/repos/containers/BranchRoot.js
Normal file
@@ -0,0 +1,139 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { Route, withRouter } from "react-router-dom";
|
||||
import Changesets from "./Changesets";
|
||||
import BranchSelector from "./BranchSelector";
|
||||
import { connect } from "react-redux";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchBranches,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "../modules/branches";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
baseUrl: string,
|
||||
selected: string,
|
||||
baseUrlWithBranch: string,
|
||||
baseUrlWithoutBranch: string,
|
||||
|
||||
// State props
|
||||
branches: Branch[],
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// Dispatch props
|
||||
fetchBranches: Repository => void,
|
||||
|
||||
// Context props
|
||||
history: any, // TODO flow type
|
||||
match: any
|
||||
};
|
||||
|
||||
class BranchRoot extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchBranches(this.props.repository);
|
||||
}
|
||||
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 1);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
branchSelected = (branch?: Branch) => {
|
||||
let url;
|
||||
if (branch) {
|
||||
url = `${this.props.baseUrlWithBranch}/${encodeURIComponent(
|
||||
branch.name
|
||||
)}/changesets/`;
|
||||
} else {
|
||||
url = `${this.props.baseUrlWithoutBranch}/`;
|
||||
}
|
||||
this.props.history.push(url);
|
||||
};
|
||||
|
||||
findSelectedBranch = () => {
|
||||
const { selected, branches } = this.props;
|
||||
return branches.find((branch: Branch) => branch.name === selected);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { repository, error, loading, match, branches } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!repository || !branches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = this.stripEndingSlash(match.url);
|
||||
const branch = this.findSelectedBranch();
|
||||
const changesets = <Changesets repository={repository} branch={branch} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderBranchSelector()}
|
||||
<Route path={`${url}/:page?`} component={() => changesets} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderBranchSelector = () => {
|
||||
const { repository, branches } = this.props;
|
||||
if (repository._links.branches) {
|
||||
return (
|
||||
<BranchSelector
|
||||
branches={branches}
|
||||
selected={(b: Branch) => {
|
||||
this.branchSelected(b);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchBranches: (repo: Repository) => {
|
||||
dispatch(fetchBranches(repo));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, match } = ownProps;
|
||||
const loading = isFetchBranchesPending(state, repository);
|
||||
const error = getFetchBranchesFailure(state, repository);
|
||||
const branches = getBranches(state, repository);
|
||||
const selected = decodeURIComponent(match.params.branch);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
branches,
|
||||
selected
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)
|
||||
)(BranchRoot);
|
||||
78
scm-ui/src/repos/containers/BranchSelector.js
Normal file
78
scm-ui/src/repos/containers/BranchSelector.js
Normal file
@@ -0,0 +1,78 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import type { Branch } from "@scm-manager/ui-types";
|
||||
import DropDown from "../components/DropDown";
|
||||
import { translate } from "react-i18next";
|
||||
import injectSheet from "react-jss";
|
||||
import { compose } from "redux";
|
||||
import classNames from "classnames";
|
||||
|
||||
const styles = {
|
||||
zeroflex: {
|
||||
flexGrow: 0
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
branches: Branch[], // TODO: Use generics?
|
||||
selected: (branch?: Branch) => void,
|
||||
|
||||
// context props
|
||||
classes: Object,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = { selectedBranch?: Branch };
|
||||
|
||||
class BranchSelector extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { branches, classes, t } = this.props;
|
||||
|
||||
if (branches) {
|
||||
return (
|
||||
<div className="box field is-horizontal">
|
||||
<div
|
||||
className={classNames("field-label", "is-normal", classes.zeroflex)}
|
||||
>
|
||||
<label className="label">{t("branch-selector.label")}</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field is-narrow">
|
||||
<div className="control">
|
||||
<DropDown
|
||||
className="is-fullwidth"
|
||||
options={branches.map(b => b.name)}
|
||||
optionSelected={this.branchSelected}
|
||||
preselectedOption={
|
||||
this.state.selectedBranch
|
||||
? this.state.selectedBranch.name
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
branchSelected = (branchName: string) => {
|
||||
const { branches, selected } = this.props;
|
||||
const branch = branches.find(b => b.name === branchName);
|
||||
|
||||
selected(branch);
|
||||
this.setState({ selectedBranch: branch });
|
||||
};
|
||||
}
|
||||
|
||||
export default compose(
|
||||
injectSheet(styles),
|
||||
translate("repos")
|
||||
)(BranchSelector);
|
||||
@@ -1,171 +1,115 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Paginator
|
||||
} from "@scm-manager/ui-components";
|
||||
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type {
|
||||
Branch,
|
||||
Changeset,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchChangesets,
|
||||
fetchChangesetsByNamespaceNameAndBranch,
|
||||
getChangesets,
|
||||
getFetchChangesetsFailure,
|
||||
isFetchChangesetsPending,
|
||||
selectListAsCollection
|
||||
} from "../modules/changesets";
|
||||
import type { History } from "history";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import ChangesetList from "../components/changesets/ChangesetList";
|
||||
import {
|
||||
fetchBranchesByNamespaceAndName,
|
||||
getBranchNames
|
||||
} from "../../repos/modules/branches";
|
||||
import type { PagedCollection, Repository } from "@scm-manager/ui-types";
|
||||
import ChangesetList from "../components/ChangesetList";
|
||||
import DropDown from "../components/DropDown";
|
||||
import { withRouter } from "react-router-dom";
|
||||
ErrorNotification,
|
||||
LinkPaginator,
|
||||
Loading,
|
||||
getPageFromMatch
|
||||
} from "@scm-manager/ui-components";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
branchName: string,
|
||||
history: History,
|
||||
fetchChangesetsByNamespaceNameAndBranch: (
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch: string
|
||||
) => void,
|
||||
list: PagedCollection
|
||||
branch: Branch,
|
||||
page: number,
|
||||
|
||||
// State props
|
||||
changesets: Changeset[],
|
||||
list: PagedCollection,
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
|
||||
// Dispatch props
|
||||
fetchChangesets: (Repository, Branch, number) => void,
|
||||
|
||||
// context props
|
||||
match: any
|
||||
};
|
||||
|
||||
class Changesets extends React.Component<State, Props> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {};
|
||||
class Changesets extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
const { namespace, name } = this.props.repository;
|
||||
const branchName = this.props.match.params.branch;
|
||||
const {
|
||||
fetchChangesetsByNamespaceNameAndBranch,
|
||||
fetchChangesetsByNamespaceAndName,
|
||||
fetchBranchesByNamespaceAndName
|
||||
} = this.props;
|
||||
if (branchName) {
|
||||
fetchChangesetsByNamespaceNameAndBranch(namespace, name, branchName);
|
||||
} else {
|
||||
fetchChangesetsByNamespaceAndName(namespace, name);
|
||||
}
|
||||
fetchBranchesByNamespaceAndName(namespace, name);
|
||||
const { fetchChangesets, repository, branch, page } = this.props;
|
||||
|
||||
fetchChangesets(repository, branch, page);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changesets, loading, error } = this.props;
|
||||
if (loading || !changesets) {
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (!changesets || changesets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<ErrorNotification error={error} />
|
||||
{this.renderTable()}
|
||||
<>
|
||||
{this.renderList()}
|
||||
{this.renderPaginator()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTable = () => {
|
||||
const branch = this.props.match.params.branch;
|
||||
const { repository, changesets, branchNames } = this.props;
|
||||
|
||||
if (branchNames && branchNames.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
<label className="label">Branch: </label>
|
||||
<DropDown
|
||||
options={branchNames}
|
||||
preselectedOption={branch}
|
||||
optionSelected={branch => this.branchChanged(branch)}
|
||||
/>
|
||||
<ChangesetList repository={repository} changesets={changesets} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderList = () => {
|
||||
const { repository, changesets } = this.props;
|
||||
return <ChangesetList repository={repository} changesets={changesets} />;
|
||||
};
|
||||
|
||||
renderPaginator() {
|
||||
const { list } = this.props;
|
||||
renderPaginator = () => {
|
||||
const { page, list } = this.props;
|
||||
if (list) {
|
||||
return <Paginator collection={list} onPageChange={this.onPageChange} />;
|
||||
return <LinkPaginator page={page} collection={list} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
branchChanged = (branchName: string): void => {
|
||||
const { history, repository } = this.props;
|
||||
history.push(
|
||||
`/repo/${repository.namespace}/${repository.name}/history/${branchName}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps: Props) => {
|
||||
const { namespace, name } = ownProps.repository;
|
||||
const loading = isFetchChangesetsPending(
|
||||
state,
|
||||
namespace,
|
||||
name,
|
||||
ownProps.match.params.branch
|
||||
);
|
||||
const changesets = getChangesets(
|
||||
state,
|
||||
namespace,
|
||||
name,
|
||||
ownProps.match.params.branch
|
||||
);
|
||||
const branchNames = getBranchNames(namespace, name, state);
|
||||
const error = getFetchChangesetsFailure(
|
||||
state,
|
||||
namespace,
|
||||
name,
|
||||
ownProps.match.params.branch
|
||||
);
|
||||
const list = selectListAsCollection(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
changesets,
|
||||
branchNames,
|
||||
error,
|
||||
list
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchChangesetsByNamespaceAndName: (namespace: string, name: string) => {
|
||||
dispatch(fetchChangesets(namespace, name));
|
||||
},
|
||||
fetchChangesetsByNamespaceNameAndBranch: (
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch: string
|
||||
) => {
|
||||
dispatch(
|
||||
fetchChangesetsByNamespaceNameAndBranch(namespace, name, branch)
|
||||
);
|
||||
},
|
||||
fetchBranchesByNamespaceAndName: (namespace: string, name: string) => {
|
||||
dispatch(fetchBranchesByNamespaceAndName(namespace, name));
|
||||
fetchChangesets: (repo: Repository, branch: Branch, page: number) => {
|
||||
dispatch(fetchChangesets(repo, branch, page));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
const mapStateToProps = (state: any, ownProps: Props) => {
|
||||
const { repository, branch, match } = ownProps;
|
||||
const changesets = getChangesets(state, repository, branch);
|
||||
const loading = isFetchChangesetsPending(state, repository, branch);
|
||||
const error = getFetchChangesetsFailure(state, repository, branch);
|
||||
const list = selectListAsCollection(state, repository, branch);
|
||||
const page = getPageFromMatch(match);
|
||||
|
||||
return { changesets, list, page, loading, error };
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Changesets)
|
||||
);
|
||||
)
|
||||
)(Changesets);
|
||||
|
||||
@@ -7,15 +7,17 @@ import {
|
||||
getRepository,
|
||||
isFetchRepoPending
|
||||
} from "../modules/repos";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import type { Repository } from "@scm-manager/ui-types";
|
||||
|
||||
import {
|
||||
Page,
|
||||
Loading,
|
||||
ErrorPage,
|
||||
Loading,
|
||||
Navigation,
|
||||
NavLink,
|
||||
Page,
|
||||
Section
|
||||
} from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
@@ -26,7 +28,8 @@ import Permissions from "../permissions/containers/Permissions";
|
||||
|
||||
import type { History } from "history";
|
||||
import EditNavLink from "../components/EditNavLink";
|
||||
import Changesets from "./Changesets";
|
||||
|
||||
import BranchRoot from "./BranchRoot";
|
||||
import ChangesetView from "./ChangesetView";
|
||||
import PermissionsNavLink from "../components/PermissionsNavLink";
|
||||
|
||||
@@ -78,6 +81,12 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
return route.location.pathname.match(`${url}/changeset/`);
|
||||
};
|
||||
|
||||
matches = (route: any) => {
|
||||
const url = this.matchedUrl();
|
||||
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
|
||||
return route.location.pathname.match(regex);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, repository, t } = this.props;
|
||||
|
||||
@@ -96,11 +105,11 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
return (
|
||||
<Page title={repository.namespace + "/" + repository.name}>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Switch>
|
||||
<Route
|
||||
path={url}
|
||||
exact
|
||||
@@ -110,21 +119,6 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
path={`${url}/edit`}
|
||||
component={() => <Edit repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/history`}
|
||||
render={() => <Changesets repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/history/:branch`}
|
||||
render={() => <Changesets repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/changeset/:id`}
|
||||
render={() => <ChangesetView repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/permissions`}
|
||||
render={props => (
|
||||
@@ -134,6 +128,32 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${url}/changeset/:id`}
|
||||
render={() => <ChangesetView repository={repository} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}/branches/:branch/changesets`}
|
||||
render={() => (
|
||||
<BranchRoot
|
||||
repository={repository}
|
||||
baseUrlWithBranch={`${url}/branches`}
|
||||
baseUrlWithoutBranch={`${url}/changesets`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
@@ -141,15 +161,15 @@ class RepositoryRoot extends React.Component<Props> {
|
||||
<NavLink to={url} label={t("repository-root.information")} />
|
||||
<NavLink
|
||||
activeOnlyWhenExact={false}
|
||||
to={`${url}/history`}
|
||||
to={`${url}/changesets/`}
|
||||
label={t("repository-root.history")}
|
||||
otherLocation={this.matchChangeset}
|
||||
activeWhenMatch={this.matches}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
<PermissionsNavLink
|
||||
permissionUrl={`${url}/permissions`}
|
||||
repository={repository}
|
||||
/>
|
||||
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
|
||||
</Section>
|
||||
<Section label={t("repository-root.actions-label")}>
|
||||
<DeleteNavAction repository={repository} delete={this.delete} />
|
||||
|
||||
@@ -1,102 +1,134 @@
|
||||
import {FAILURE_SUFFIX, PENDING_SUFFIX, SUCCESS_SUFFIX} from "../../modules/types";
|
||||
// @flow
|
||||
import {
|
||||
FAILURE_SUFFIX,
|
||||
PENDING_SUFFIX,
|
||||
SUCCESS_SUFFIX
|
||||
} from "../../modules/types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import type { Action, Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
|
||||
export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES";
|
||||
export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`;
|
||||
export const FETCH_BRANCHES_SUCCESS = `${FETCH_BRANCHES}_${SUCCESS_SUFFIX}`;
|
||||
export const FETCH_BRANCHES_FAILURE = `${FETCH_BRANCHES}_${FAILURE_SUFFIX}`;
|
||||
|
||||
const REPO_URL = "repositories";
|
||||
|
||||
// Fetching branches
|
||||
export function fetchBranchesByNamespaceAndName(namespace: string, name: string) {
|
||||
|
||||
export function fetchBranches(repository: Repository) {
|
||||
if (!repository._links.branches) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { repository, data: {} },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchBranchesPending(namespace, name));
|
||||
return apiClient.get(REPO_URL + "/" + namespace + "/" + name + "/branches")
|
||||
dispatch(fetchBranchesPending(repository));
|
||||
return apiClient
|
||||
.get(repository._links.branches.href)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchBranchesSuccess(data, namespace, name))
|
||||
dispatch(fetchBranchesSuccess(data, repository));
|
||||
})
|
||||
.catch(cause => {
|
||||
dispatch(fetchBranchesFailure(namespace, name, cause))
|
||||
})
|
||||
}
|
||||
.catch(error => {
|
||||
dispatch(fetchBranchesFailure(repository, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Action creators
|
||||
export function fetchBranchesPending(namespace: string, name: string) {
|
||||
export function fetchBranchesPending(repository: Repository) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: {namespace, name},
|
||||
itemId: namespace + "/" + name
|
||||
}
|
||||
payload: { repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchesSuccess(data: string, namespace: string, name: string) {
|
||||
export function fetchBranchesSuccess(data: string, repository: Repository) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: {data, namespace, name},
|
||||
itemId: namespace + "/" + name
|
||||
}
|
||||
payload: { data, repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBranchesFailure(namespace: string, name: string, error: Error) {
|
||||
export function fetchBranchesFailure(repository: Repository, error: Error) {
|
||||
return {
|
||||
type: FETCH_BRANCHES_FAILURE,
|
||||
payload: {error, namespace, name},
|
||||
itemId: namespace + "/" + name
|
||||
}
|
||||
payload: { error, repository },
|
||||
itemId: createKey(repository)
|
||||
};
|
||||
}
|
||||
|
||||
// Reducers
|
||||
|
||||
export default function reducer(state: Object = {}, action: Action = {type: "UNKNOWN"}): Object {
|
||||
type State = { [string]: Branch[] };
|
||||
|
||||
export default function reducer(
|
||||
state: State = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): State {
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_BRANCHES_SUCCESS:
|
||||
const key = action.payload.namespace + "/" + action.payload.name;
|
||||
let oldBranchesByNames = {[key]: {}};
|
||||
if (state[key] !== undefined) {
|
||||
oldBranchesByNames[key] = state[key]
|
||||
}
|
||||
const key = createKey(payload.repository);
|
||||
return {
|
||||
[key]: {
|
||||
byNames: extractBranchesByNames(action.payload.data, oldBranchesByNames[key].byNames)
|
||||
}
|
||||
...state,
|
||||
[key]: extractBranchesFromPayload(payload.data)
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function extractBranchesByNames(data: any, oldBranchesByNames: any): Branch[] {
|
||||
const branches = data._embedded.branches;
|
||||
const branchesByNames = {};
|
||||
|
||||
for (let branch of branches) {
|
||||
branchesByNames[branch.name] = branch;
|
||||
function extractBranchesFromPayload(payload: any) {
|
||||
if (payload._embedded && payload._embedded.branches) {
|
||||
return payload._embedded.branches;
|
||||
}
|
||||
|
||||
for (let name in oldBranchesByNames) {
|
||||
branchesByNames[name] = oldBranchesByNames[name]
|
||||
}
|
||||
return branchesByNames;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Selectors
|
||||
|
||||
export function getBranchesForNamespaceAndNameFromState(namespace: string, name: string, state: Object) {
|
||||
const key = namespace + "/" + name;
|
||||
if (!state.branches[key]) {
|
||||
return null;
|
||||
export function getBranches(state: Object, repository: Repository) {
|
||||
const key = createKey(repository);
|
||||
if (state.branches[key]) {
|
||||
return state.branches[key];
|
||||
}
|
||||
return Object.values(state.branches[key].byNames);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getBranchNames(namespace: string, name: string, state: Object) {
|
||||
const key = namespace + "/" + name;
|
||||
if (!state.branches[key] || !state.branches[key].byNames) {
|
||||
export function getBranch(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
name: string
|
||||
): ?Branch {
|
||||
const key = createKey(repository);
|
||||
if (state.branches[key]) {
|
||||
return state.branches[key].find((b: Branch) => b.name === name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return Object.keys(state.branches[key].byNames);
|
||||
|
||||
export function isFetchBranchesPending(
|
||||
state: Object,
|
||||
repository: Repository
|
||||
): boolean {
|
||||
return isPending(state, FETCH_BRANCHES, createKey(repository));
|
||||
}
|
||||
|
||||
export function getFetchBranchesFailure(state: Object, repository: Repository) {
|
||||
return getFailure(state, FETCH_BRANCHES, createKey(repository));
|
||||
}
|
||||
|
||||
function createKey(repository: Repository): string {
|
||||
const { namespace, name } = repository;
|
||||
return `${namespace}/${name}`;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {
|
||||
import reducer, {
|
||||
FETCH_BRANCHES,
|
||||
FETCH_BRANCHES_FAILURE,
|
||||
FETCH_BRANCHES_PENDING,
|
||||
FETCH_BRANCHES_SUCCESS,
|
||||
fetchBranchesByNamespaceAndName,
|
||||
getBranchesForNamespaceAndNameFromState,
|
||||
getBranchNames
|
||||
fetchBranches,
|
||||
getBranch,
|
||||
getBranches,
|
||||
getFetchBranchesFailure,
|
||||
isFetchBranchesPending
|
||||
} from "./branches";
|
||||
import reducer from "./branches";
|
||||
|
||||
const namespace = "foo";
|
||||
const name = "bar";
|
||||
const key = namespace + "/" + name;
|
||||
const repository = {
|
||||
namespace: "foo",
|
||||
name: "bar",
|
||||
_links: {
|
||||
branches: {
|
||||
href: "http://scm/api/rest/v2/repositories/foo/bar/branches"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const branch1 = { name: "branch1", revision: "revision1" };
|
||||
const branch2 = { name: "branch2", revision: "revision2" };
|
||||
const branch3 = { name: "branch3", revision: "revision3" };
|
||||
|
||||
describe("branches", () => {
|
||||
describe("fetch branches", () => {
|
||||
const URL = "/api/v2/repositories/foo/bar/branches";
|
||||
const URL = "http://scm/api/rest/v2/repositories/foo/bar/branches";
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
@@ -36,20 +48,18 @@ describe("fetch branches", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { namespace, name },
|
||||
payload: { repository },
|
||||
itemId: key
|
||||
},
|
||||
{
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: { data: collection, namespace, name },
|
||||
payload: { data: collection, repository },
|
||||
itemId: key
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchBranchesByNamespaceAndName(namespace, name))
|
||||
.then(() => {
|
||||
return store.dispatch(fetchBranches(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -62,20 +72,18 @@ describe("fetch branches", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_BRANCHES_PENDING,
|
||||
payload: { namespace, name },
|
||||
payload: { repository },
|
||||
itemId: key
|
||||
},
|
||||
{
|
||||
type: FETCH_BRANCHES_FAILURE,
|
||||
payload: { error: collection, namespace, name },
|
||||
payload: { error: collection, repository },
|
||||
itemId: key
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchBranchesByNamespaceAndName(namespace, name))
|
||||
.then(() => {
|
||||
return store.dispatch(fetchBranches(repository)).then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_BRANCHES_FAILURE);
|
||||
});
|
||||
@@ -91,8 +99,7 @@ describe("branches reducer", () => {
|
||||
const action = {
|
||||
type: FETCH_BRANCHES_SUCCESS,
|
||||
payload: {
|
||||
namespace,
|
||||
name,
|
||||
repository,
|
||||
data: branches
|
||||
}
|
||||
};
|
||||
@@ -101,61 +108,88 @@ describe("branches reducer", () => {
|
||||
const newState = reducer({}, action);
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState[key]).toBeDefined();
|
||||
expect(newState[key].byNames["branch1"]).toEqual(branch1);
|
||||
expect(newState[key].byNames["branch2"]).toEqual(branch2);
|
||||
expect(newState[key]).toContain(branch1);
|
||||
expect(newState[key]).toContain(branch2);
|
||||
});
|
||||
|
||||
it("should not delete existing branches from state", () => {
|
||||
const oldState = {
|
||||
"foo/bar": {
|
||||
byNames: {
|
||||
branch3: branch3
|
||||
}
|
||||
}
|
||||
"hitchhiker/heartOfGold": [branch3]
|
||||
};
|
||||
|
||||
const newState = reducer(oldState, action);
|
||||
console.log(newState);
|
||||
expect(newState[key].byNames["branch1"]).toEqual(branch1);
|
||||
expect(newState[key].byNames["branch2"]).toEqual(branch2);
|
||||
expect(newState[key].byNames["branch3"]).toEqual(branch3);
|
||||
expect(newState[key]).toContain(branch1);
|
||||
expect(newState[key]).toContain(branch2);
|
||||
|
||||
expect(newState["hitchhiker/heartOfGold"]).toContain(branch3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("branch selectors", () => {
|
||||
it("should get branches for namespace and name", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
const state = {
|
||||
branches: {
|
||||
[key]: {
|
||||
byNames: {
|
||||
branch1: branch1
|
||||
}
|
||||
}
|
||||
[key]: [branch1, branch2]
|
||||
}
|
||||
};
|
||||
const branches = getBranchesForNamespaceAndNameFromState(
|
||||
namespace,
|
||||
name,
|
||||
state
|
||||
);
|
||||
expect(branches.length).toEqual(1);
|
||||
expect(branches[0]).toEqual(branch1);
|
||||
|
||||
it("should return true, when fetching branches is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_BRANCHES + "/foo/bar"]: true
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchBranchesPending(state, repository)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return branches names", () => {
|
||||
it("should return branches", () => {
|
||||
const branches = getBranches(state, repository);
|
||||
expect(branches.length).toEqual(2);
|
||||
expect(branches).toContain(branch1);
|
||||
expect(branches).toContain(branch2);
|
||||
});
|
||||
|
||||
it("should return always the same reference for branches", () => {
|
||||
const one = getBranches(state, repository);
|
||||
const two = getBranches(state, repository);
|
||||
expect(one).toBe(two);
|
||||
});
|
||||
|
||||
it("should return null, if no branches for the repository available", () => {
|
||||
const branches = getBranches({ branches: {} }, repository);
|
||||
expect(branches).toBeNull();
|
||||
});
|
||||
|
||||
it("should return single branch by name", () => {
|
||||
const branch = getBranch(state, repository, "branch1");
|
||||
expect(branch).toEqual(branch1);
|
||||
});
|
||||
|
||||
it("should return same reference for single branch by name", () => {
|
||||
const one = getBranch(state, repository, "branch1");
|
||||
const two = getBranch(state, repository, "branch1");
|
||||
expect(one).toBe(two);
|
||||
});
|
||||
|
||||
it("should return undefined if branch does not exist", () => {
|
||||
const branch = getBranch(state, repository, "branch42");
|
||||
expect(branch).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return error if fetching branches failed", () => {
|
||||
const state = {
|
||||
branches: {
|
||||
[key]: {
|
||||
byNames: {
|
||||
branch1: branch1,
|
||||
branch2: branch2
|
||||
}
|
||||
}
|
||||
failure: {
|
||||
[FETCH_BRANCHES + "/foo/bar"]: error
|
||||
}
|
||||
};
|
||||
const names = getBranchNames(namespace, name, state);
|
||||
expect(names.length).toEqual(2);
|
||||
expect(names).toContain("branch1");
|
||||
expect(names).toContain("branch2");
|
||||
|
||||
expect(getFetchBranchesFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching branches did not fail", () => {
|
||||
expect(getFetchBranchesFailure({}, repository)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,12 @@ import {
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
import { combineReducers } from "redux";
|
||||
import type { Action, PagedCollection, Repository } from "@scm-manager/ui-types";
|
||||
import * as types from "../../modules/types";
|
||||
import type {
|
||||
Action,
|
||||
Branch,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
|
||||
export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS";
|
||||
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`;
|
||||
@@ -27,15 +30,11 @@ export const FETCH_CHANGESET_FAILURE = `${FETCH_CHANGESET}_${FAILURE_SUFFIX}`;
|
||||
//********end of detailed view add
|
||||
|
||||
// actions
|
||||
const REPO_URL = "repositories";
|
||||
//TODO: Content type
|
||||
|
||||
//********added for detailed view of changesets
|
||||
|
||||
export function fetchChangesetIfNeeded(
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
export function fetchChangesetIfNeeded(repository: Repository, id: string) {
|
||||
return (dispatch: any, getState: any) => {
|
||||
if (shouldFetchChangeset(getState(), repository, id)) {
|
||||
return dispatch(fetchChangeset(repository, id));
|
||||
@@ -43,18 +42,13 @@ export function fetchChangesetIfNeeded(
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangeset(
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
export function fetchChangeset(repository: Repository, id: string) {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchChangesetPending(repository, id));
|
||||
return apiClient
|
||||
.get(REPO_URL + `/${repository.namespace}/${repository.name}/changesets/${id}`)
|
||||
.get(repository._links.changesets.href + id)
|
||||
.then(response => response.json())
|
||||
.then(data =>
|
||||
dispatch(fetchChangesetSuccess(data, repository, id))
|
||||
)
|
||||
.then(data => dispatch(fetchChangesetSuccess(data, repository, id)))
|
||||
.catch(err => {
|
||||
dispatch(fetchChangesetFailure(repository, id, err));
|
||||
});
|
||||
@@ -67,11 +61,7 @@ export function fetchChangesetPending(
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
payload: {
|
||||
repository,
|
||||
id
|
||||
},
|
||||
itemId: createItemId(repository.namespace, repository.name, id)
|
||||
itemId: createChangesetItemId(repository, id)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +73,7 @@ export function fetchChangesetSuccess(
|
||||
return {
|
||||
type: FETCH_CHANGESET_SUCCESS,
|
||||
payload: { changeset, repository, id },
|
||||
itemId: createItemId(repository.namespace, repository.name, id)
|
||||
itemId: createChangesetItemId(repository, id)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,194 +89,167 @@ function fetchChangesetFailure(
|
||||
id,
|
||||
error
|
||||
},
|
||||
itemId: createItemId(repository.namespace, repository.name, id)
|
||||
itemId: createChangesetItemId(repository, id)
|
||||
};
|
||||
}
|
||||
|
||||
//********end of detailed view add
|
||||
|
||||
export function fetchChangesetsWithOptions(
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string,
|
||||
suffix?: string
|
||||
export function fetchChangesets(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
page?: number
|
||||
) {
|
||||
let link = REPO_URL + `/${namespace}/${name}`;
|
||||
if (branch && branch !== "") {
|
||||
link = link + `/branches/${branch}`;
|
||||
}
|
||||
link = link + "/changesets";
|
||||
if (suffix) {
|
||||
link = link + `${suffix}`;
|
||||
}
|
||||
const link = createChangesetsLink(repository, branch, page);
|
||||
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchChangesetsPending(namespace, name, branch));
|
||||
dispatch(fetchChangesetsPending(repository, branch));
|
||||
return apiClient
|
||||
.get(link)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
dispatch(fetchChangesetsSuccess(data, namespace, name, branch));
|
||||
dispatch(fetchChangesetsSuccess(repository, branch, data));
|
||||
})
|
||||
.catch(cause => {
|
||||
dispatch(fetchChangesetsFailure(namespace, name, cause, branch));
|
||||
dispatch(fetchChangesetsFailure(repository, branch, cause));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangesets(namespace: string, name: string) {
|
||||
return fetchChangesetsWithOptions(namespace, name);
|
||||
function createChangesetsLink(
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
page?: number
|
||||
) {
|
||||
let link = repository._links.changesets.href;
|
||||
|
||||
if (branch) {
|
||||
link = branch._links.history.href;
|
||||
}
|
||||
|
||||
export function fetchChangesetsByPage(
|
||||
namespace: string,
|
||||
name: string,
|
||||
page: number
|
||||
) {
|
||||
return fetchChangesetsWithOptions(namespace, name, "", `?page=${page}`);
|
||||
if (page) {
|
||||
link = link + `?page=${page - 1}`;
|
||||
}
|
||||
|
||||
export function fetchChangesetsByBranchAndPage(
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch: string,
|
||||
page: number
|
||||
) {
|
||||
return fetchChangesetsWithOptions(namespace, name, branch, `?page=${page}`);
|
||||
}
|
||||
|
||||
export function fetchChangesetsByNamespaceNameAndBranch(
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch: string
|
||||
) {
|
||||
return fetchChangesetsWithOptions(namespace, name, branch);
|
||||
return link;
|
||||
}
|
||||
|
||||
export function fetchChangesetsPending(
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): Action {
|
||||
const itemId = createItemId(namespace, name, branch);
|
||||
const itemId = createItemId(repository, branch);
|
||||
|
||||
return {
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: itemId,
|
||||
itemId
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchChangesetsSuccess(
|
||||
changesets: any,
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
changesets: any
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: createItemId(namespace, name, branch)
|
||||
itemId: createItemId(repository, branch)
|
||||
};
|
||||
}
|
||||
|
||||
function fetchChangesetsFailure(
|
||||
namespace: string,
|
||||
name: string,
|
||||
error: Error,
|
||||
branch?: string
|
||||
repository: Repository,
|
||||
branch?: Branch,
|
||||
error: Error
|
||||
): Action {
|
||||
return {
|
||||
type: FETCH_CHANGESETS_FAILURE,
|
||||
payload: {
|
||||
namespace,
|
||||
name,
|
||||
branch,
|
||||
error
|
||||
repository,
|
||||
error,
|
||||
branch
|
||||
},
|
||||
itemId: createItemId(namespace, name, branch)
|
||||
itemId: createItemId(repository, branch)
|
||||
};
|
||||
}
|
||||
|
||||
function createItemId(
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string
|
||||
): string {
|
||||
function createChangesetItemId(repository: Repository, id: string) {
|
||||
const { namespace, name } = repository;
|
||||
return namespace + "/" + name + "/" + id;
|
||||
}
|
||||
|
||||
function createItemId(repository: Repository, branch?: Branch): string {
|
||||
const { namespace, name } = repository;
|
||||
let itemId = namespace + "/" + name;
|
||||
if (branch && branch !== "") {
|
||||
itemId = itemId + "/" + branch;
|
||||
if (branch) {
|
||||
itemId = itemId + "/" + branch.name;
|
||||
}
|
||||
return itemId;
|
||||
}
|
||||
|
||||
// reducer
|
||||
function byKeyReducer(
|
||||
export default function reducer(
|
||||
state: any = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
switch (action.type) {
|
||||
//********added for detailed view of changesets
|
||||
case FETCH_CHANGESET_SUCCESS:
|
||||
const _key = createItemId(
|
||||
action.payload.repository.namespace,
|
||||
action.payload.repository.name
|
||||
);
|
||||
let _oldChangesets = { [_key]: {} };
|
||||
if (state[_key] !== undefined) {
|
||||
_oldChangesets[_key] = state[_key];
|
||||
if (!action.payload) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const payload = action.payload;
|
||||
switch (action.type) {
|
||||
case FETCH_CHANGESET_SUCCESS:
|
||||
const _key = createItemId(payload.repository);
|
||||
|
||||
let _oldByIds = {};
|
||||
if (state[_key] && state[_key].byId) {
|
||||
_oldByIds = state[_key].byId;
|
||||
}
|
||||
|
||||
const changeset = payload.changeset;
|
||||
|
||||
return {
|
||||
...state,
|
||||
[_key]: {
|
||||
byId: addChangesetToChangesets(
|
||||
action.payload.changeset,
|
||||
_oldChangesets[_key].byId
|
||||
)
|
||||
...state[_key],
|
||||
byId: {
|
||||
..._oldByIds,
|
||||
[changeset.id]: changeset
|
||||
}
|
||||
}
|
||||
};
|
||||
//********end of added for detailed view of changesets
|
||||
|
||||
case FETCH_CHANGESETS_SUCCESS:
|
||||
const changesets = payload._embedded.changesets;
|
||||
const changesetIds = changesets.map(c => c.id);
|
||||
const key = action.itemId;
|
||||
let oldChangesets = { [key]: {} };
|
||||
if (state[key] !== undefined) {
|
||||
oldChangesets[key] = state[key];
|
||||
|
||||
if (!key) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let oldByIds = {};
|
||||
if (state[key] && state[key].byId) {
|
||||
oldByIds = state[key].byId;
|
||||
}
|
||||
|
||||
const byIds = extractChangesetsByIds(changesets);
|
||||
|
||||
return {
|
||||
...state,
|
||||
[key]: {
|
||||
byId: extractChangesetsByIds(action.payload, oldChangesets[key].byId)
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function listReducer(
|
||||
state: any = {},
|
||||
action: Action = { type: "UNKNOWN" }
|
||||
): Object {
|
||||
switch (action.type) {
|
||||
//********added for detailed view of changesets
|
||||
case FETCH_CHANGESET_SUCCESS:
|
||||
const changesetId = action.payload.changeset.id;
|
||||
const stateEntries = state.entries ? state.entries : [];
|
||||
stateEntries.push(changesetId);
|
||||
return {
|
||||
entries: stateEntries,
|
||||
entry: {
|
||||
...state.entry
|
||||
}
|
||||
};
|
||||
//********end of added for detailed view of changesets
|
||||
case FETCH_CHANGESETS_SUCCESS:
|
||||
const changesets = action.payload._embedded.changesets;
|
||||
const changesetIds = changesets.map(c => c.id);
|
||||
return {
|
||||
byId: {
|
||||
...oldByIds,
|
||||
...byIds
|
||||
},
|
||||
list: {
|
||||
entries: changesetIds,
|
||||
entry: {
|
||||
page: action.payload.page,
|
||||
pageTotal: action.payload.pageTotal,
|
||||
_links: action.payload._links
|
||||
page: payload.page,
|
||||
pageTotal: payload.pageTotal,
|
||||
_links: payload._links
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
default:
|
||||
@@ -294,23 +257,13 @@ function listReducer(
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
list: listReducer,
|
||||
byKey: byKeyReducer
|
||||
});
|
||||
|
||||
function extractChangesetsByIds(data: any, oldChangesetsByIds: any) {
|
||||
const changesets = data._embedded.changesets;
|
||||
function extractChangesetsByIds(changesets: any) {
|
||||
const changesetsByIds = {};
|
||||
|
||||
for (let changeset of changesets) {
|
||||
changesetsByIds[changeset.id] = changeset;
|
||||
}
|
||||
|
||||
for (let id in oldChangesetsByIds) {
|
||||
changesetsByIds[id] = oldChangesetsByIds[id];
|
||||
}
|
||||
|
||||
return changesetsByIds;
|
||||
}
|
||||
//********added for detailed view of changesets
|
||||
@@ -332,28 +285,30 @@ function addChangesetToChangesets(data: any, oldChangesetsByIds: any) {
|
||||
//selectors
|
||||
export function getChangesets(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
const key = createItemId(namespace, name, branch);
|
||||
if (!state.changesets.byKey[key]) {
|
||||
const key = createItemId(repository, branch);
|
||||
|
||||
const changesets = state.changesets[key];
|
||||
if (!changesets) {
|
||||
return null;
|
||||
}
|
||||
return Object.values(state.changesets.byKey[key].byId);
|
||||
return changesets.list.entries.map((id: string) => {
|
||||
return changesets.byId[id];
|
||||
});
|
||||
}
|
||||
|
||||
//********added for detailed view of changesets
|
||||
export function getChangeset(
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
id: string,
|
||||
branch?: string
|
||||
id: string
|
||||
) {
|
||||
const key = createItemId(repository.namespace, repository.name, branch);
|
||||
const key = createItemId(repository);
|
||||
const changesets =
|
||||
state.changesets && state.changesets.byKey && state.changesets.byKey[key]
|
||||
? state.changesets.byKey[key].byId
|
||||
state.changesets && state.changesets[key]
|
||||
? state.changesets[key].byId
|
||||
: null;
|
||||
if (changesets != null && changesets[id]) {
|
||||
return changesets[id];
|
||||
@@ -377,7 +332,11 @@ export function isFetchChangesetPending(
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
return isPending(state, FETCH_CHANGESET, createItemId(repository.namespace, repository.name, id));
|
||||
return isPending(
|
||||
state,
|
||||
FETCH_CHANGESET,
|
||||
createChangesetItemId(repository, id)
|
||||
);
|
||||
}
|
||||
|
||||
export function getFetchChangesetFailure(
|
||||
@@ -385,51 +344,54 @@ export function getFetchChangesetFailure(
|
||||
repository: Repository,
|
||||
id: string
|
||||
) {
|
||||
return getFailure(state, FETCH_CHANGESET, createItemId(repository.namespace, repository.name, id));
|
||||
return getFailure(
|
||||
state,
|
||||
FETCH_CHANGESET,
|
||||
createChangesetItemId(repository, id)
|
||||
);
|
||||
}
|
||||
//********end of added for detailed view of changesets
|
||||
|
||||
export function isFetchChangesetsPending(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
return isPending(
|
||||
state,
|
||||
FETCH_CHANGESETS,
|
||||
createItemId(namespace, name, branch)
|
||||
);
|
||||
return isPending(state, FETCH_CHANGESETS, createItemId(repository, branch));
|
||||
}
|
||||
|
||||
export function getFetchChangesetsFailure(
|
||||
state: Object,
|
||||
namespace: string,
|
||||
name: string,
|
||||
branch?: string
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
) {
|
||||
return getFailure(
|
||||
state,
|
||||
FETCH_CHANGESETS,
|
||||
createItemId(namespace, name, branch)
|
||||
);
|
||||
return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch));
|
||||
}
|
||||
|
||||
const selectList = (state: Object) => {
|
||||
if (state.changesets && state.changesets.list) {
|
||||
return state.changesets.list;
|
||||
const selectList = (state: Object, repository: Repository, branch?: Branch) => {
|
||||
const itemId = createItemId(repository, branch);
|
||||
if (state.changesets[itemId] && state.changesets[itemId].list) {
|
||||
return state.changesets[itemId].list;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const selectListEntry = (state: Object): Object => {
|
||||
const list = selectList(state);
|
||||
const selectListEntry = (
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): Object => {
|
||||
const list = selectList(state, repository, branch);
|
||||
if (list.entry) {
|
||||
return list.entry;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const selectListAsCollection = (state: Object): PagedCollection => {
|
||||
return selectListEntry(state);
|
||||
export const selectListAsCollection = (
|
||||
state: Object,
|
||||
repository: Repository,
|
||||
branch?: Branch
|
||||
): PagedCollection => {
|
||||
return selectListEntry(state, repository, branch);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import {
|
||||
import reducer, {
|
||||
FETCH_CHANGESETS,
|
||||
FETCH_CHANGESETS_FAILURE,
|
||||
FETCH_CHANGESETS_PENDING,
|
||||
@@ -13,9 +13,6 @@ import {
|
||||
FETCH_CHANGESET_PENDING,
|
||||
FETCH_CHANGESET_SUCCESS,
|
||||
fetchChangesets,
|
||||
fetchChangesetsByBranchAndPage,
|
||||
fetchChangesetsByNamespaceNameAndBranch,
|
||||
fetchChangesetsByPage,
|
||||
fetchChangesetsSuccess,
|
||||
getChangesets,
|
||||
getFetchChangesetsFailure,
|
||||
@@ -28,21 +25,45 @@ import {
|
||||
getFetchChangesetFailure,
|
||||
fetchChangesetSuccess
|
||||
} from "./changesets";
|
||||
import reducer from "./changesets";
|
||||
|
||||
const changesets = {};
|
||||
//********added for detailed view of changesets
|
||||
const branch = {
|
||||
name: "specific",
|
||||
revision: "123",
|
||||
_links: {
|
||||
history: {
|
||||
href:
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/branches/specific/changesets"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const repository = {
|
||||
namespace: "foo",
|
||||
name: "bar"
|
||||
name: "bar",
|
||||
type: "GIT",
|
||||
_links: {
|
||||
self: {
|
||||
href: "http://scm.hitchhicker.com/api/v2/repositories/foo/bar"
|
||||
},
|
||||
changesets: {
|
||||
href: "http://scm.hitchhicker.com/api/v2/repositories/foo/bar/changesets"
|
||||
},
|
||||
branches: {
|
||||
href:
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/branches/specific/branches"
|
||||
}
|
||||
}
|
||||
};
|
||||
//********end of added for detailed view of changesets
|
||||
|
||||
const changesets = {};
|
||||
|
||||
describe("changesets", () => {
|
||||
describe("fetching of changesets", () => {
|
||||
const DEFAULT_BRANCH_URL = "/api/v2/repositories/foo/bar/changesets";
|
||||
const DEFAULT_BRANCH_URL =
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/changesets";
|
||||
const SPECIFIC_BRANCH_URL =
|
||||
"/api/v2/repositories/foo/bar/branches/specific/changesets";
|
||||
"http://scm.hitchhicker.com/api/v2/repositories/foo/bar/branches/specific/changesets";
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
|
||||
afterEach(() => {
|
||||
@@ -50,7 +71,6 @@ describe("changesets", () => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
//********added for detailed view of changesets
|
||||
const changesetId = "aba876c0625d90a6aff1494f3d161aaa7008b958";
|
||||
|
||||
it("should fetch changeset", () => {
|
||||
@@ -59,13 +79,6 @@ describe("changesets", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
payload: {
|
||||
id: changesetId,
|
||||
repository: {
|
||||
name: "bar",
|
||||
namespace: "foo"
|
||||
}
|
||||
},
|
||||
itemId: "foo/bar/" + changesetId
|
||||
},
|
||||
{
|
||||
@@ -73,10 +86,7 @@ describe("changesets", () => {
|
||||
payload: {
|
||||
changeset: {},
|
||||
id: changesetId,
|
||||
repository: {
|
||||
name: "bar",
|
||||
namespace: "foo"
|
||||
}
|
||||
repository: repository
|
||||
},
|
||||
itemId: "foo/bar/" + changesetId
|
||||
}
|
||||
@@ -96,13 +106,6 @@ describe("changesets", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
payload: {
|
||||
id: changesetId,
|
||||
repository: {
|
||||
name: "bar",
|
||||
namespace: "foo"
|
||||
}
|
||||
},
|
||||
itemId: "foo/bar/" + changesetId
|
||||
}
|
||||
];
|
||||
@@ -118,44 +121,21 @@ describe("changesets", () => {
|
||||
});
|
||||
|
||||
it("should fetch changeset if needed", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/" + "id3", "{}");
|
||||
|
||||
const state = {
|
||||
changesets: {
|
||||
byKey: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/id3", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESET_PENDING,
|
||||
payload: {
|
||||
id: "id3",
|
||||
repository: {
|
||||
name: "bar",
|
||||
namespace: "foo"
|
||||
}
|
||||
},
|
||||
itemId: "foo/bar/" + "id3"
|
||||
itemId: "foo/bar/id3"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESET_SUCCESS,
|
||||
payload: {
|
||||
changeset: {},
|
||||
id: "id3",
|
||||
repository: {
|
||||
name: "bar",
|
||||
namespace: "foo"
|
||||
}
|
||||
repository: repository
|
||||
},
|
||||
itemId: "foo/bar/" + "id3"
|
||||
itemId: "foo/bar/id3"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -168,7 +148,7 @@ describe("changesets", () => {
|
||||
});
|
||||
|
||||
it("should not fetch changeset if not needed", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/" + "id1", 500);
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "/id1", 500);
|
||||
|
||||
const state = {
|
||||
changesets: {
|
||||
@@ -183,23 +163,18 @@ describe("changesets", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const expectedActions = [];
|
||||
|
||||
const store = mockStore(state);
|
||||
return expect(
|
||||
store.dispatch(fetchChangesetIfNeeded(repository, "id1"))
|
||||
).toEqual(undefined);
|
||||
});
|
||||
|
||||
//********end of added for detailed view of changesets
|
||||
|
||||
it("should fetch changesets for default branch", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL, "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: "foo/bar",
|
||||
itemId: "foo/bar"
|
||||
},
|
||||
{
|
||||
@@ -210,7 +185,7 @@ describe("changesets", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets("foo", "bar")).then(() => {
|
||||
return store.dispatch(fetchChangesets(repository)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -222,7 +197,6 @@ describe("changesets", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: itemId,
|
||||
itemId
|
||||
},
|
||||
{
|
||||
@@ -233,11 +207,7 @@ describe("changesets", () => {
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(
|
||||
fetchChangesetsByNamespaceNameAndBranch("foo", "bar", "specific")
|
||||
)
|
||||
.then(() => {
|
||||
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
@@ -249,13 +219,12 @@ describe("changesets", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: itemId,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets("foo", "bar")).then(() => {
|
||||
return store.dispatch(fetchChangesets(repository)).then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE);
|
||||
expect(store.getActions()[1].payload).toBeDefined();
|
||||
@@ -269,17 +238,12 @@ describe("changesets", () => {
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: itemId,
|
||||
itemId
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(
|
||||
fetchChangesetsByNamespaceNameAndBranch("foo", "bar", "specific")
|
||||
)
|
||||
.then(() => {
|
||||
return store.dispatch(fetchChangesets(repository, branch)).then(() => {
|
||||
expect(store.getActions()[0]).toEqual(expectedActions[0]);
|
||||
expect(store.getActions()[1].type).toEqual(FETCH_CHANGESETS_FAILURE);
|
||||
expect(store.getActions()[1].payload).toBeDefined();
|
||||
@@ -287,12 +251,11 @@ describe("changesets", () => {
|
||||
});
|
||||
|
||||
it("should fetch changesets by page", () => {
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "?page=5", "{}");
|
||||
fetchMock.getOnce(DEFAULT_BRANCH_URL + "?page=4", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: "foo/bar",
|
||||
itemId: "foo/bar"
|
||||
},
|
||||
{
|
||||
@@ -302,35 +265,34 @@ describe("changesets", () => {
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesetsByPage("foo", "bar", 5)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets by branch and page", () => {
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL + "?page=5", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
payload: "foo/bar/specific",
|
||||
itemId: "foo/bar/specific"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: "foo/bar/specific"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store
|
||||
.dispatch(fetchChangesetsByBranchAndPage("foo", "bar", "specific", 5))
|
||||
.dispatch(fetchChangesets(repository, undefined, 5))
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch changesets by branch and page", () => {
|
||||
fetchMock.getOnce(SPECIFIC_BRANCH_URL + "?page=4", "{}");
|
||||
|
||||
const expectedActions = [
|
||||
{
|
||||
type: FETCH_CHANGESETS_PENDING,
|
||||
itemId: "foo/bar/specific"
|
||||
},
|
||||
{
|
||||
type: FETCH_CHANGESETS_SUCCESS,
|
||||
payload: changesets,
|
||||
itemId: "foo/bar/specific"
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchChangesets(repository, branch, 5)).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("changesets reducer", () => {
|
||||
@@ -355,19 +317,15 @@ describe("changesets", () => {
|
||||
it("should set state to received changesets", () => {
|
||||
const newState = reducer(
|
||||
{},
|
||||
fetchChangesetsSuccess(responseBody, "foo", "bar")
|
||||
fetchChangesetsSuccess(repository, undefined, responseBody)
|
||||
);
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState.byKey["foo/bar"].byId["changeset1"].author.mail).toEqual(
|
||||
expect(newState["foo/bar"].byId["changeset1"].author.mail).toEqual(
|
||||
"z@phod.com"
|
||||
);
|
||||
expect(newState.byKey["foo/bar"].byId["changeset2"].description).toEqual(
|
||||
"foo"
|
||||
);
|
||||
expect(newState.byKey["foo/bar"].byId["changeset3"].description).toEqual(
|
||||
"bar"
|
||||
);
|
||||
expect(newState.list).toEqual({
|
||||
expect(newState["foo/bar"].byId["changeset2"].description).toEqual("foo");
|
||||
expect(newState["foo/bar"].byId["changeset3"].description).toEqual("bar");
|
||||
expect(newState["foo/bar"].list).toEqual({
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
@@ -377,36 +335,33 @@ describe("changesets", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should not delete existing changesets from state", () => {
|
||||
const responseBody = {
|
||||
_embedded: {
|
||||
changesets: [
|
||||
{ id: "changeset1", author: { mail: "z@phod.com", name: "zaphod" } }
|
||||
],
|
||||
_embedded: {
|
||||
tags: [],
|
||||
branches: [],
|
||||
parents: []
|
||||
it("should not remove existing changesets", () => {
|
||||
const state = {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
list: {
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = reducer(
|
||||
{
|
||||
byKey: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
["changeset2"]: {
|
||||
id: "changeset2",
|
||||
author: { mail: "mail@author.com", name: "author" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
fetchChangesetsSuccess(responseBody, "foo", "bar")
|
||||
state,
|
||||
fetchChangesetsSuccess(repository, undefined, responseBody)
|
||||
);
|
||||
expect(newState.byKey["foo/bar"].byId["changeset2"]).toBeDefined();
|
||||
expect(newState.byKey["foo/bar"].byId["changeset1"]).toBeDefined();
|
||||
|
||||
const fooBar = newState["foo/bar"];
|
||||
|
||||
expect(fooBar.list.entries).toEqual([
|
||||
"changeset1",
|
||||
"changeset2",
|
||||
"changeset3"
|
||||
]);
|
||||
expect(fooBar.byId["id2"]).toEqual({ id: "id2" });
|
||||
expect(fooBar.byId["id1"]).toEqual({ id: "id1" });
|
||||
});
|
||||
|
||||
//********added for detailed view of changesets
|
||||
@@ -428,15 +383,12 @@ describe("changesets", () => {
|
||||
it("should add changeset to state", () => {
|
||||
const newState = reducer(
|
||||
{
|
||||
byKey: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
["id2"]: {
|
||||
id: "id2",
|
||||
author: { mail: "mail@author.com", name: "author" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
list: {
|
||||
entry: {
|
||||
@@ -446,35 +398,32 @@ describe("changesets", () => {
|
||||
},
|
||||
entries: ["id2"]
|
||||
}
|
||||
}
|
||||
},
|
||||
fetchChangesetSuccess(responseBodySingleChangeset, repository, "id3")
|
||||
);
|
||||
|
||||
expect(newState).toBeDefined();
|
||||
expect(newState.byKey["foo/bar"].byId["id3"].description).toEqual(
|
||||
expect(newState["foo/bar"].byId["id3"].description).toEqual(
|
||||
"added testChangeset"
|
||||
);
|
||||
expect(newState.byKey["foo/bar"].byId["id3"].author.mail).toEqual(
|
||||
"z@phod.com"
|
||||
);
|
||||
expect(newState.byKey["foo/bar"].byId["id2"]).toBeDefined();
|
||||
expect(newState.byKey["foo/bar"].byId["id3"]).toBeDefined();
|
||||
expect(newState.list).toEqual({
|
||||
expect(newState["foo/bar"].byId["id3"].author.mail).toEqual("z@phod.com");
|
||||
expect(newState["foo/bar"].byId["id2"]).toBeDefined();
|
||||
expect(newState["foo/bar"].byId["id3"]).toBeDefined();
|
||||
expect(newState["foo/bar"].list).toEqual({
|
||||
entry: {
|
||||
page: 1,
|
||||
pageTotal: 10,
|
||||
_links: {}
|
||||
},
|
||||
entries: ["id2", "id3"]
|
||||
entries: ["id2"]
|
||||
});
|
||||
});
|
||||
//********end of added for detailed view of changesets
|
||||
});
|
||||
|
||||
describe("changeset selectors", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
|
||||
//********added for detailed view of changesets
|
||||
|
||||
it("should return changeset", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
@@ -570,23 +519,25 @@ describe("changesets", () => {
|
||||
it("should return false if fetching changeset did not fail", () => {
|
||||
expect(getFetchChangesetFailure({}, repository, "id1")).toBeUndefined();
|
||||
});
|
||||
|
||||
//********end of added for detailed view of changesets
|
||||
|
||||
it("should get all changesets for a given namespace and name", () => {
|
||||
it("should get all changesets for a given repository", () => {
|
||||
const state = {
|
||||
changesets: {
|
||||
byKey: {
|
||||
"foo/bar": {
|
||||
byId: {
|
||||
id1: { id: "id1" },
|
||||
id2: { id: "id2" }
|
||||
}
|
||||
id2: { id: "id2" },
|
||||
id1: { id: "id1" }
|
||||
},
|
||||
list: {
|
||||
entries: ["id1", "id2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = getChangesets(state, "foo", "bar");
|
||||
expect(result).toContainEqual({ id: "id1" });
|
||||
const result = getChangesets(state, repository);
|
||||
expect(result).toEqual([{ id: "id1" }, { id: "id2" }]);
|
||||
});
|
||||
|
||||
it("should return true, when fetching changesets is pending", () => {
|
||||
@@ -596,11 +547,11 @@ describe("changesets", () => {
|
||||
}
|
||||
};
|
||||
|
||||
expect(isFetchChangesetsPending(state, "foo", "bar")).toBeTruthy();
|
||||
expect(isFetchChangesetsPending(state, repository)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false, when fetching changesets is not pending", () => {
|
||||
expect(isFetchChangesetsPending({}, "foo", "bar")).toEqual(false);
|
||||
expect(isFetchChangesetsPending({}, repository)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error if fetching changesets failed", () => {
|
||||
@@ -610,11 +561,11 @@ describe("changesets", () => {
|
||||
}
|
||||
};
|
||||
|
||||
expect(getFetchChangesetsFailure(state, "foo", "bar")).toEqual(error);
|
||||
expect(getFetchChangesetsFailure(state, repository)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return false if fetching changesets did not fail", () => {
|
||||
expect(getFetchChangesetsFailure({}, "foo", "bar")).toBeUndefined();
|
||||
expect(getFetchChangesetsFailure({}, repository)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
Select
|
||||
} from "@scm-manager/ui-components";
|
||||
import { Select } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
@@ -15,7 +13,7 @@ type Props = {
|
||||
class TypeSelector extends React.Component<Props> {
|
||||
render() {
|
||||
const { type, handleTypeChange, loading } = this.props;
|
||||
const types = ["READ", "OWNER", "WRITE"];
|
||||
const types = ["READ", "WRITE", "OWNER"];
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -5,12 +5,15 @@ import "../../../../tests/i18n";
|
||||
import DeletePermissionButton from "./DeletePermissionButton";
|
||||
|
||||
import { confirmAlert } from "@scm-manager/ui-components";
|
||||
import ReactRouterEnzymeContext from "react-router-enzyme-context";
|
||||
jest.mock("@scm-manager/ui-components", () => ({
|
||||
confirmAlert: jest.fn(),
|
||||
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
|
||||
}));
|
||||
|
||||
describe("DeletePermissionButton", () => {
|
||||
const options = new ReactRouterEnzymeContext();
|
||||
|
||||
it("should render nothing, if the delete link is missing", () => {
|
||||
const permission = {
|
||||
_links: {}
|
||||
@@ -20,7 +23,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).toBe("");
|
||||
});
|
||||
@@ -38,7 +42,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
expect(navLink.text()).not.toBe("");
|
||||
});
|
||||
@@ -56,7 +61,8 @@ describe("DeletePermissionButton", () => {
|
||||
<DeletePermissionButton
|
||||
permission={permission}
|
||||
deletePermission={() => {}}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
button.find("button").simulate("click");
|
||||
|
||||
@@ -82,7 +88,8 @@ describe("DeletePermissionButton", () => {
|
||||
permission={permission}
|
||||
confirmDialog={false}
|
||||
deletePermission={capture}
|
||||
/>
|
||||
/>,
|
||||
options.get()
|
||||
);
|
||||
button.find("button").simulate("click");
|
||||
|
||||
|
||||
@@ -645,9 +645,9 @@
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85"
|
||||
|
||||
"@scm-manager/ui-bundler@^0.0.15":
|
||||
version "0.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917"
|
||||
"@scm-manager/ui-bundler@^0.0.17":
|
||||
version "0.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.17.tgz#949b90ca57e4268be28fcf4975bd9622f60278bb"
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/plugin-proposal-class-properties" "^7.0.0"
|
||||
@@ -664,7 +664,6 @@
|
||||
browserify-css "^0.14.0"
|
||||
colors "^1.3.1"
|
||||
commander "^2.17.1"
|
||||
connect-history-api-fallback "^1.5.0"
|
||||
eslint "^5.4.0"
|
||||
eslint-config-react-app "^2.1.0"
|
||||
eslint-plugin-flowtype "^2.50.0"
|
||||
@@ -1980,7 +1979,7 @@ concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0:
|
||||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
connect-history-api-fallback@^1, connect-history-api-fallback@^1.5.0:
|
||||
connect-history-api-fallback@^1:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
|
||||
|
||||
@@ -6574,7 +6573,7 @@ rc@^1.2.7:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-dom@^16.4.2:
|
||||
react-dom@^16.4.2, react-dom@^16.5.2:
|
||||
version "16.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
|
||||
dependencies:
|
||||
@@ -6663,7 +6662,7 @@ react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.1:
|
||||
react-is "^16.5.2"
|
||||
schedule "^0.5.0"
|
||||
|
||||
react@^16.4.2:
|
||||
react@^16.4.2, react@^16.5.2:
|
||||
version "16.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42"
|
||||
dependencies:
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ManagerDaoAdapter<T extends ModelObject> {
|
||||
this.dao = dao;
|
||||
}
|
||||
|
||||
public void modify(T object, Function<T, PermissionCheck> permissionCheck, AroundHandler<T> beforeUpdate, AroundHandler<T> afterUpdate) throws NotFoundException {
|
||||
public void modify(T object, Function<T, PermissionCheck> permissionCheck, AroundHandler<T> beforeUpdate, AroundHandler<T> afterUpdate) {
|
||||
T notModified = dao.get(object.getId());
|
||||
if (notModified != null) {
|
||||
permissionCheck.apply(notModified).check();
|
||||
@@ -51,7 +51,7 @@ public class ManagerDaoAdapter<T extends ModelObject> {
|
||||
return newObject;
|
||||
}
|
||||
|
||||
public void delete(T toDelete, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeDelete, AroundHandler<T> afterDelete) throws NotFoundException {
|
||||
public void delete(T toDelete, Supplier<PermissionCheck> permissionCheck, AroundHandler<T> beforeDelete, AroundHandler<T> afterDelete) {
|
||||
permissionCheck.get().check();
|
||||
if (dao.contains(toDelete)) {
|
||||
beforeDelete.handle(toDelete);
|
||||
|
||||
@@ -63,6 +63,6 @@ public class IllegalArgumentExceptionMapper
|
||||
public Response toResponse(IllegalArgumentException exception)
|
||||
{
|
||||
log.info("caught IllegalArgumentException -- mapping to bad request", exception);
|
||||
return Response.status(Status.BAD_REQUEST).build();
|
||||
return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public class ChangePasswordResource
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
|
||||
public Response changePassword(@FormParam("old-password") String oldPassword, @FormParam("new-password") String newPassword) throws NotFoundException, ConcurrentModificationException {
|
||||
public Response changePassword(@FormParam("old-password") String oldPassword, @FormParam("new-password") String newPassword) {
|
||||
AssertUtil.assertIsNotEmpty(oldPassword);
|
||||
AssertUtil.assertIsNotEmpty(newPassword);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class DiffRootResource {
|
||||
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws NotFoundException {
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision){
|
||||
HttpUtil.checkForCRLFInjection(revision);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
StreamingOutput responseEntry = output -> {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class GroupResource {
|
||||
@ResponseCode(code = 404, condition = "not found, no group with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("id") String id) throws NotFoundException {
|
||||
public Response get(@PathParam("id") String id){
|
||||
return adapter.get(id, groupToGroupDtoMapper::map);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public class GroupResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
public Response update(@PathParam("id") String name, @Valid GroupDto groupDto) throws NotFoundException, ConcurrentModificationException {
|
||||
public Response update(@PathParam("id") String name, @Valid GroupDto groupDto) throws ConcurrentModificationException {
|
||||
return adapter.update(name, existing -> dtoToGroupMapper.map(groupDto));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@ import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.Manager;
|
||||
import sonia.scm.ModelObject;
|
||||
import sonia.scm.NotFoundException;
|
||||
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;
|
||||
@@ -34,20 +32,11 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
||||
collectionAdapter = new CollectionResourceManagerAdapter<>(manager, type);
|
||||
}
|
||||
|
||||
Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) throws NotFoundException {
|
||||
Response get(String id, Function<MODEL_OBJECT, DTO> mapToDto) {
|
||||
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 {
|
||||
public Response update(String id, Function<MODEL_OBJECT, MODEL_OBJECT> applyChanges) throws ConcurrentModificationException {
|
||||
return singleAdapter.update(
|
||||
loadBy(id),
|
||||
applyChanges,
|
||||
|
||||
@@ -5,14 +5,12 @@ 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.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -22,9 +20,6 @@ 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;
|
||||
|
||||
|
||||
/**
|
||||
@@ -60,7 +55,7 @@ public class MeResource {
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException {
|
||||
public Response get(@Context Request request, @Context UriInfo uriInfo) {
|
||||
|
||||
String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
|
||||
return adapter.get(id, meToUserDtoMapper::map);
|
||||
@@ -78,19 +73,8 @@ public class MeResource {
|
||||
})
|
||||
@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);
|
||||
}
|
||||
};
|
||||
public Response changePassword(@Valid PasswordChangeDto passwordChangeDto) {
|
||||
userManager.changePasswordForLoggedInUser(passwordService.encryptPassword(passwordChangeDto.getOldPassword()), passwordService.encryptPassword(passwordChangeDto.getNewPassword()));
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.hibernate.validator.constraints.NotEmpty;
|
||||
@ToString
|
||||
public class PasswordChangeDto {
|
||||
|
||||
@NotEmpty
|
||||
private String oldPassword;
|
||||
|
||||
@NotEmpty
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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 PasswordOverwriteDto {
|
||||
@NotEmpty
|
||||
private String newPassword;
|
||||
}
|
||||
@@ -100,7 +100,7 @@ public class PermissionRootResource {
|
||||
@Produces(VndMediaType.PERMISSION)
|
||||
@TypeHint(PermissionDto.class)
|
||||
@Path("{permission-name}")
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) throws NotFoundException {
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName) {
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionRead(repository).check();
|
||||
return Response.ok(
|
||||
@@ -158,7 +158,7 @@ public class PermissionRootResource {
|
||||
public Response update(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("permission-name") String permissionName,
|
||||
@Valid PermissionDto permission) throws NotFoundException, AlreadyExistsException {
|
||||
@Valid PermissionDto permission) throws AlreadyExistsException {
|
||||
log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission);
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.permissionWrite(repository).check();
|
||||
@@ -198,7 +198,7 @@ public class PermissionRootResource {
|
||||
@Path("{permission-name}")
|
||||
public Response delete(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("permission-name") String permissionName) throws NotFoundException {
|
||||
@PathParam("permission-name") String permissionName) {
|
||||
log.info("try to delete the permission with name: {}.", permissionName);
|
||||
Repository repository = load(namespace, name);
|
||||
RepositoryPermissions.modify(repository).check();
|
||||
|
||||
@@ -91,7 +91,7 @@ public class RepositoryResource {
|
||||
@ResponseCode(code = 404, condition = "not found, no repository with the specified name available in the namespace"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name) throws NotFoundException {
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name){
|
||||
return adapter.get(loadBy(namespace, name), repositoryToDtoMapper::map);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ public class RepositoryResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repositoryDto) throws NotFoundException, ConcurrentModificationException {
|
||||
public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid RepositoryDto repositoryDto) throws ConcurrentModificationException {
|
||||
return adapter.update(
|
||||
loadBy(namespace, name),
|
||||
existing -> processUpdate(repositoryDto, existing),
|
||||
|
||||
@@ -87,7 +87,7 @@ class ResourceLinks {
|
||||
}
|
||||
|
||||
public String passwordChange(String name) {
|
||||
return userLinkBuilder.method("getUserResource").parameters(name).method("changePassword").parameters().href();
|
||||
return userLinkBuilder.method("getUserResource").parameters(name).method("overwritePassword").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
||||
* Reads the model object for the given id, transforms it to a dto and returns a corresponding http response.
|
||||
* This handles all corner cases, eg. no matching object for the id or missing privileges.
|
||||
*/
|
||||
Response get(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, DTO> mapToDto) throws NotFoundException {
|
||||
Response get(Supplier<Optional<MODEL_OBJECT>> reader, Function<MODEL_OBJECT, DTO> mapToDto) {
|
||||
return reader.get()
|
||||
.map(mapToDto)
|
||||
.map(Response::ok)
|
||||
|
||||
@@ -47,7 +47,7 @@ public class SourceRootResource {
|
||||
@GET
|
||||
@Produces(VndMediaType.SOURCE)
|
||||
@Path("{revision}/{path: .*}")
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws NotFoundException, IOException {
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision, @PathParam("path") String path) throws IOException {
|
||||
return getSource(namespace, name, path, revision);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -57,7 +56,7 @@ public class UserResource {
|
||||
@ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("id") String id) throws NotFoundException {
|
||||
public Response get(@PathParam("id") String id) {
|
||||
return adapter.get(id, userToDtoMapper::map);
|
||||
}
|
||||
|
||||
@@ -102,7 +101,7 @@ public class UserResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException {
|
||||
public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws ConcurrentModificationException {
|
||||
return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword()));
|
||||
}
|
||||
|
||||
@@ -111,13 +110,15 @@ public class UserResource {
|
||||
* 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.
|
||||
* <strong>Note:</strong> This method requires "user:modify" privilege to modify the password of other users.
|
||||
* <strong>Note:</strong> This method requires "user:changeOwnPassword" privilege to modify the own password.
|
||||
*
|
||||
* @param name name of the user to be modified
|
||||
* @param passwordChangeDto change password object to modify password. the old password is here not required
|
||||
* @param passwordOverwriteDto change password object to modify password. the old password is here not required
|
||||
*/
|
||||
@PUT
|
||||
@Path("password")
|
||||
@Consumes(VndMediaType.PASSWORD_CHANGE)
|
||||
@Consumes(VndMediaType.PASSWORD_OVERWRITE)
|
||||
@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"),
|
||||
@@ -127,8 +128,8 @@ public class UserResource {
|
||||
@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());
|
||||
public Response overwritePassword(@PathParam("id") String name, @Valid PasswordOverwriteDto passwordOverwriteDto) {
|
||||
userManager.overwritePassword(name, passwordService.encryptPassword(passwordOverwriteDto.getNewPassword()));
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ public class DefaultGroupManager extends AbstractGroupManager
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Group group) throws NotFoundException {
|
||||
public void delete(Group group){
|
||||
logger.info("delete group {} of type {}", group.getName(), group.getType());
|
||||
managerDaoAdapter.delete(
|
||||
group,
|
||||
@@ -145,7 +145,7 @@ public class DefaultGroupManager extends AbstractGroupManager
|
||||
public void init(SCMContextProvider context) {}
|
||||
|
||||
@Override
|
||||
public void modify(Group group) throws NotFoundException {
|
||||
public void modify(Group group){
|
||||
logger.info("modify group {} of type {}", group.getName(), group.getType());
|
||||
|
||||
managerDaoAdapter.modify(
|
||||
@@ -160,7 +160,7 @@ public class DefaultGroupManager extends AbstractGroupManager
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh(Group group) throws NotFoundException {
|
||||
public void refresh(Group group){
|
||||
String name = group.getName();
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
|
||||
@@ -151,7 +151,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Repository repository) throws NotFoundException {
|
||||
public void delete(Repository repository){
|
||||
logger.info("delete repository {}/{} of type {}", repository.getNamespace(), repository.getName(), repository.getType());
|
||||
managerDaoAdapter.delete(
|
||||
repository,
|
||||
@@ -179,7 +179,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(Repository repository) throws NotFoundException {
|
||||
public void modify(Repository repository){
|
||||
logger.info("modify repository {}/{} of type {}", repository.getNamespace(), repository.getName(), repository.getType());
|
||||
|
||||
managerDaoAdapter.modify(
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class HealthChecker {
|
||||
this.repositoryManager = repositoryManager;
|
||||
}
|
||||
|
||||
public void check(String id) throws NotFoundException {
|
||||
public void check(String id){
|
||||
RepositoryPermissions.healthCheck(id).check();
|
||||
|
||||
Repository repository = repositoryManager.get(id);
|
||||
@@ -68,7 +68,7 @@ public final class HealthChecker {
|
||||
}
|
||||
|
||||
public void check(Repository repository)
|
||||
throws NotFoundException, ConcurrentModificationException {
|
||||
{
|
||||
RepositoryPermissions.healthCheck(repository).check();
|
||||
|
||||
doCheck(repository);
|
||||
@@ -83,7 +83,7 @@ public final class HealthChecker {
|
||||
if (check.isPermitted(repository)) {
|
||||
try {
|
||||
check(repository);
|
||||
} catch (ConcurrentModificationException | NotFoundException ex) {
|
||||
} catch (NotFoundException ex) {
|
||||
logger.error("health check ends with exception", ex);
|
||||
}
|
||||
} else {
|
||||
@@ -94,7 +94,7 @@ public final class HealthChecker {
|
||||
}
|
||||
}
|
||||
|
||||
private void doCheck(Repository repository) throws NotFoundException {
|
||||
private void doCheck(Repository repository){
|
||||
logger.info("start health check for repository {}", repository.getName());
|
||||
|
||||
HealthCheckResult result = HealthCheckResult.healthy();
|
||||
|
||||
@@ -260,6 +260,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
builder.add(canReadOwnUser(user));
|
||||
builder.add(getUserAutocompletePermission());
|
||||
builder.add(getGroupAutocompletePermission());
|
||||
builder.add(getChangeOwnPasswordPermission(user));
|
||||
permissions = builder.build();
|
||||
}
|
||||
|
||||
@@ -272,6 +273,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector
|
||||
return GroupPermissions.autocomplete().asShiroString();
|
||||
}
|
||||
|
||||
private String getChangeOwnPasswordPermission(User user) {
|
||||
return UserPermissions.changePassword(user).asShiroString();
|
||||
}
|
||||
|
||||
private String getUserAutocompletePermission() {
|
||||
return UserPermissions.autocomplete().asShiroString();
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
|
||||
|
||||
private String subject;
|
||||
private String issuer;
|
||||
private long expiresIn = 10l;
|
||||
private long expiresIn = 60l;
|
||||
private TimeUnit expiresInUnit = TimeUnit.MINUTES;
|
||||
private Scope scope = Scope.empty();
|
||||
|
||||
|
||||
@@ -33,11 +33,10 @@
|
||||
|
||||
package sonia.scm.user;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.github.sdorra.ssp.PermissionActionCheck;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
@@ -64,8 +63,6 @@ import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
@@ -157,7 +154,7 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(User user) throws NotFoundException {
|
||||
public void delete(User user) {
|
||||
logger.info("delete user {} of type {}", user.getName(), user.getType());
|
||||
managerDaoAdapter.delete(
|
||||
user,
|
||||
@@ -193,9 +190,8 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void modify(User user) throws NotFoundException {
|
||||
public void modify(User user) {
|
||||
logger.info("modify user {} of type {}", user.getName(), user.getType());
|
||||
|
||||
managerDaoAdapter.modify(
|
||||
user,
|
||||
UserPermissions::modify,
|
||||
@@ -212,7 +208,7 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void refresh(User user) throws NotFoundException {
|
||||
public void refresh(User user) {
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
logger.info("refresh user {} of type {}", user.getName(), user.getType());
|
||||
@@ -402,6 +398,36 @@ public class DefaultUserManager extends AbstractUserManager
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void changePasswordForLoggedInUser(String oldPassword, String newPassword) {
|
||||
User user = get((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal());
|
||||
|
||||
if (!user.getPassword().equals(oldPassword)) {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
|
||||
user.setPassword(newPassword);
|
||||
|
||||
managerDaoAdapter.modify(
|
||||
user,
|
||||
UserPermissions::changePassword,
|
||||
notModified -> fireEvent(HandlerEventType.BEFORE_MODIFY, user, notModified),
|
||||
notModified -> fireEvent(HandlerEventType.MODIFY, user, notModified));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwritePassword(String userId, String newPassword) {
|
||||
User user = get(userId);
|
||||
if (user == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!isTypeDefault(user)) {
|
||||
throw new ChangePasswordNotAllowedException(user.getType());
|
||||
}
|
||||
user.setPassword(newPassword);
|
||||
this.modify(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.jboss.resteasy.mock.MockDispatcherFactory;
|
||||
import sonia.scm.api.rest.AlreadyExistsExceptionMapper;
|
||||
import sonia.scm.api.rest.AuthorizationExceptionMapper;
|
||||
import sonia.scm.api.rest.ConcurrentModificationExceptionMapper;
|
||||
import sonia.scm.api.rest.IllegalArgumentExceptionMapper;
|
||||
|
||||
public class DispatcherMock {
|
||||
public static Dispatcher createDispatcher(Object resource) {
|
||||
@@ -17,6 +18,7 @@ public class DispatcherMock {
|
||||
dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class);
|
||||
dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class);
|
||||
dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class);
|
||||
dispatcher.getProviderFactory().registerProvider(IllegalArgumentExceptionMapper.class);
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.user.InvalidPasswordException;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.lang.model.util.Types;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@@ -26,6 +28,7 @@ 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.doThrow;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
@@ -69,7 +72,6 @@ public class MeResourceTest {
|
||||
doNothing().when(userManager).modify(userCaptor.capture());
|
||||
doNothing().when(userManager).delete(userCaptor.capture());
|
||||
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("/"));
|
||||
@@ -97,38 +99,40 @@ public class MeResourceTest {
|
||||
public void shouldEncryptPasswordBeforeChanging() throws Exception {
|
||||
String newPassword = "pwd123";
|
||||
String encryptedNewPassword = "encrypted123";
|
||||
String oldPassword = "notEncriptedSecret";
|
||||
String encryptedOldPassword = "encryptedOld";
|
||||
String oldPassword = "secret";
|
||||
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");
|
||||
|
||||
when(passwordService.encryptPassword(newPassword)).thenReturn(encryptedNewPassword);
|
||||
when(passwordService.encryptPassword(oldPassword)).thenReturn(encryptedOldPassword);
|
||||
|
||||
ArgumentCaptor<String> encryptedOldPasswordCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> encryptedNewPasswordCaptor = ArgumentCaptor.forClass(String.class);
|
||||
doNothing().when(userManager).changePasswordForLoggedInUser(encryptedOldPasswordCaptor.capture(), encryptedNewPasswordCaptor.capture());
|
||||
|
||||
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());
|
||||
assertEquals(encryptedNewPassword, encryptedNewPasswordCaptor.getValue());
|
||||
assertEquals(encryptedOldPassword, encryptedOldPasswordCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
|
||||
public void shouldGet400OnMissingOldPassword() throws Exception {
|
||||
originalUser.setType("not an xml type");
|
||||
String newPassword = "pwd123";
|
||||
String oldPassword = "notEncriptedSecret";
|
||||
String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword);
|
||||
String content = String.format("{ \"newPassword\": \"%s\" }", 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);
|
||||
|
||||
@@ -137,17 +141,34 @@ public class MeResourceTest {
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldGet400OnChangePasswordIfOldPasswordDoesNotMatchOriginalPassword() throws Exception {
|
||||
public void shouldGet400OnMissingEmptyPassword() throws Exception {
|
||||
String newPassword = "pwd123";
|
||||
String oldPassword = "notEncriptedSecret";
|
||||
String oldPassword = "";
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(username = "trillian", password = "secret")
|
||||
public void shouldMapExceptionFromManager() throws Exception {
|
||||
String newPassword = "pwd123";
|
||||
String oldPassword = "secret";
|
||||
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();
|
||||
|
||||
doThrow(InvalidPasswordException.class).when(userManager).changePasswordForLoggedInUser(any(), any());
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.user.ChangePasswordNotAllowedException;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
@@ -31,6 +33,7 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -69,7 +72,6 @@ public class UserRootResourceTest {
|
||||
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");
|
||||
@@ -143,7 +145,7 @@ public class UserRootResourceTest {
|
||||
String content = String.format("{\"newPassword\": \"%s\"}", newPassword);
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password")
|
||||
.contentType(VndMediaType.PASSWORD_CHANGE)
|
||||
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||
.content(content.getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
||||
@@ -151,26 +153,61 @@ public class UserRootResourceTest {
|
||||
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());
|
||||
verify(userManager).overwritePassword("Neo", "encrypted123");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception {
|
||||
public void shouldGet400OnOverwritePasswordWhenManagerThrowsNotAllowed() 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)
|
||||
.contentType(VndMediaType.PASSWORD_OVERWRITE)
|
||||
.content(content.getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
doThrow(ChangePasswordNotAllowedException.class).when(userManager).overwritePassword(any(), any());
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGet404OnOverwritePasswordWhenNotFound() 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_OVERWRITE)
|
||||
.content(content.getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
doThrow(NotFoundException.class).when(userManager).overwritePassword(any(), any());
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEncryptPasswordOnOverwritePassword() 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_OVERWRITE)
|
||||
.content(content.getBytes());
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
|
||||
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
|
||||
verify(userManager).overwritePassword("Neo", "encrypted123");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -65,7 +65,7 @@ public class UserToUserDtoMapperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
|
||||
public void shouldGetPasswordLinkForAdmin() {
|
||||
User user = createDefaultUser();
|
||||
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
|
||||
when(userManager.isTypeDefault(eq(user))).thenReturn(true);
|
||||
@@ -73,14 +73,15 @@ public class UserToUserDtoMapperTest {
|
||||
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());
|
||||
|
||||
@Test
|
||||
public void shouldGetPasswordLinkOnlyForDefaultUserType() {
|
||||
User user = createDefaultUser();
|
||||
when(subject.isPermitted("user:modify:abc")).thenReturn(true);
|
||||
when(userManager.isTypeDefault(eq(user))).thenReturn(false);
|
||||
|
||||
userDto = mapper.map(user);
|
||||
UserDto userDto = mapper.map(user);
|
||||
|
||||
assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent());
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void testDeleteNotFound() throws NotFoundException {
|
||||
public void testDeleteNotFound(){
|
||||
manager.delete(createRepositoryWithId());
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModify() throws NotFoundException, AlreadyExistsException {
|
||||
public void testModify() throws AlreadyExistsException {
|
||||
Repository heartOfGold = createTestRepository();
|
||||
|
||||
heartOfGold.setDescription("prototype ship");
|
||||
@@ -328,12 +328,12 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void testModifyNotFound() throws NotFoundException {
|
||||
public void testModifyNotFound(){
|
||||
manager.modify(createRepositoryWithId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefresh() throws NotFoundException, AlreadyExistsException {
|
||||
public void testRefresh() throws AlreadyExistsException {
|
||||
Repository heartOfGold = createTestRepository();
|
||||
String description = heartOfGold.getDescription();
|
||||
|
||||
@@ -354,7 +354,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
}
|
||||
|
||||
@Test(expected = RepositoryNotFoundException.class)
|
||||
public void testRefreshNotFound() throws NotFoundException {
|
||||
public void testRefreshNotFound(){
|
||||
manager.refresh(createRepositoryWithId());
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
|
||||
return createRepository(RepositoryTestData.createHeartOfGold());
|
||||
}
|
||||
|
||||
private void delete(Manager<Repository> manager, Repository repository) throws NotFoundException {
|
||||
private void delete(Manager<Repository> manager, Repository repository){
|
||||
|
||||
String id = repository.getId();
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserTestData;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
@@ -124,8 +123,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||
)
|
||||
public void testCollectFromCache()
|
||||
{
|
||||
public void testCollectFromCache() {
|
||||
AuthorizationInfo info = new SimpleAuthorizationInfo();
|
||||
when(cache.get(anyObject())).thenReturn(info);
|
||||
authenticate(UserTestData.createTrillian(), "main");
|
||||
@@ -155,14 +153,13 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||
)
|
||||
public void testCollectWithoutPermissions()
|
||||
{
|
||||
public void testCollectWithoutPermissions() {
|
||||
authenticate(UserTestData.createTrillian(), "main");
|
||||
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.contains(Role.USER));
|
||||
assertThat(authInfo.getStringPermissions(), hasSize(3));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:read:trillian"));
|
||||
assertThat(authInfo.getStringPermissions(), hasSize(4));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "user:read:trillian"));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
}
|
||||
|
||||
@@ -173,8 +170,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||
)
|
||||
public void testCollectAsAdmin()
|
||||
{
|
||||
public void testCollectAsAdmin() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
trillian.setAdmin(true);
|
||||
authenticate(trillian, "main");
|
||||
@@ -192,8 +188,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
@SubjectAware(
|
||||
configuration = "classpath:sonia/scm/shiro-001.ini"
|
||||
)
|
||||
public void testCollectWithRepositoryPermissions()
|
||||
{
|
||||
public void testCollectWithRepositoryPermissions() {
|
||||
String group = "heart-of-gold-crew";
|
||||
authenticate(UserTestData.createTrillian(), group);
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
@@ -209,7 +204,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:autocomplete", "group:autocomplete", "user:changePassword:trillian", "repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,7 +225,7 @@ public class DefaultAuthorizationCollectorTest {
|
||||
AuthorizationInfo authInfo = collector.collect();
|
||||
assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER));
|
||||
assertThat(authInfo.getObjectPermissions(), nullValue());
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete" , "group:autocomplete" ));
|
||||
assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian", "user:autocomplete", "group:autocomplete", "user:changePassword:trillian"));
|
||||
}
|
||||
|
||||
private void authenticate(User user, String group, String... groups) {
|
||||
|
||||
@@ -39,9 +39,13 @@ import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
import sonia.scm.user.xml.XmlUserDAO;
|
||||
import sonia.scm.util.MockUtil;
|
||||
@@ -70,6 +74,10 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
|
||||
private UserDAO userDAO = mock(UserDAO.class);
|
||||
private User trillian;
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -82,6 +90,16 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
||||
return new DefaultUserManager(createXmlUserDAO());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void initDao() {
|
||||
trillian = UserTestData.createTrillian();
|
||||
trillian.setPassword("oldEncrypted");
|
||||
|
||||
userDAO = mock(UserDAO.class);
|
||||
when(userDAO.getType()).thenReturn("xml");
|
||||
when(userDAO.get("trillian")).thenReturn(trillian);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -89,7 +107,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
||||
@Test
|
||||
public void testDefaultAccountAfterFristStart()
|
||||
{
|
||||
UserDAO userDAO = mock(UserDAO.class);
|
||||
List<User> users = Lists.newArrayList(new User("tuser"));
|
||||
|
||||
when(userDAO.getAll()).thenReturn(users);
|
||||
@@ -108,8 +125,6 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testDefaultAccountCreation()
|
||||
{
|
||||
UserDAO userDAO = mock(UserDAO.class);
|
||||
|
||||
when(userDAO.getAll()).thenReturn(Collections.EMPTY_LIST);
|
||||
|
||||
UserManager userManager = new DefaultUserManager(userDAO);
|
||||
@@ -118,6 +133,55 @@ public class DefaultUserManagerTest extends UserManagerTestBase
|
||||
verify(userDAO, times(2)).add(any(User.class));
|
||||
}
|
||||
|
||||
@Test(expected = InvalidPasswordException.class)
|
||||
public void shouldFailChangePasswordForWrongOldPassword() {
|
||||
UserManager userManager = new DefaultUserManager(userDAO);
|
||||
|
||||
userManager.changePasswordForLoggedInUser("wrongPassword", "---");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSucceedChangePassword() {
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
|
||||
doNothing().when(userDAO).modify(userCaptor.capture());
|
||||
|
||||
UserManager userManager = new DefaultUserManager(userDAO);
|
||||
|
||||
userManager.changePasswordForLoggedInUser("oldEncrypted", "newEncrypted");
|
||||
|
||||
Assertions.assertThat(userCaptor.getValue().getPassword()).isEqualTo("newEncrypted");
|
||||
}
|
||||
|
||||
@Test(expected = ChangePasswordNotAllowedException.class)
|
||||
public void shouldFailOverwritePasswordForWrongType() {
|
||||
trillian.setType("wrongType");
|
||||
|
||||
UserManager userManager = new DefaultUserManager(userDAO);
|
||||
|
||||
userManager.overwritePassword("trillian", "---");
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void shouldFailOverwritePasswordForMissingUser() {
|
||||
UserManager userManager = new DefaultUserManager(userDAO);
|
||||
|
||||
userManager.overwritePassword("notExisting", "---");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSucceedOverwritePassword() {
|
||||
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
|
||||
|
||||
doNothing().when(userDAO).modify(userCaptor.capture());
|
||||
|
||||
UserManager userManager = new DefaultUserManager(userDAO);
|
||||
|
||||
userManager.overwritePassword("trillian", "newEncrypted");
|
||||
|
||||
Assertions.assertThat(userCaptor.getValue().getPassword()).isEqualTo("newEncrypted");
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
private XmlUserDAO createXmlUserDAO() {
|
||||
|
||||
Reference in New Issue
Block a user