From d5b54f40747c8d5d2f4f11409f57397bda814a59 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Tue, 16 Dec 2014 18:40:08 +0100 Subject: [PATCH] SONAR-5934 Simple User Creation WS --- .../server/platform/ServerComponents.java | 3 + .../org/sonar/server/user/UserService.java | 18 ++- .../sonar/server/user/index/UserIndex.java | 59 +++++++++ .../sonar/server/user/index/UserIndexer.java | 19 +++ .../user/index/UserResultSetIterator.java | 5 +- .../sonar/server/user/ws/CreateAction.java | 117 ++++++++++++++++++ .../org/sonar/server/user/ws/UsersWs.java | 42 ++----- .../server/user/UserServiceMediumTest.java | 36 ++++++ .../server/user/index/UserIndexTest.java | 87 +++++++++++++ .../server/user/index/UserIndexerTest.java | 1 + .../org/sonar/server/user/ws/UsersWsTest.java | 5 +- .../UserIndexTest/get_nullable_by_login.json | 9 ++ 12 files changed, 360 insertions(+), 41 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java create mode 100644 server/sonar-server/src/test/resources/org/sonar/server/user/index/UserIndexTest/get_nullable_by_login.json diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java index ef0bdb16177..26fb763fce5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java @@ -184,6 +184,7 @@ import org.sonar.server.user.*; import org.sonar.server.user.db.GroupDao; import org.sonar.server.user.db.UserDao; import org.sonar.server.user.db.UserGroupDao; +import org.sonar.server.user.index.UserIndex; import org.sonar.server.user.index.UserIndexDefinition; import org.sonar.server.user.index.UserIndexer; import org.sonar.server.user.ws.FavoritesWs; @@ -468,10 +469,12 @@ class ServerComponents { pico.addSingleton(DefaultUserFinder.class); pico.addSingleton(DefaultUserService.class); pico.addSingleton(UsersWs.class); + pico.addSingleton(org.sonar.server.user.ws.CreateAction.class); pico.addSingleton(FavoritesWs.class); pico.addSingleton(UserPropertiesWs.class); pico.addSingleton(UserIndexDefinition.class); pico.addSingleton(UserIndexer.class); + pico.addSingleton(UserIndex.class); pico.addSingleton(UserService.class); pico.addSingleton(UserCreator.class); diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserService.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserService.java index e7f504f64a2..9f6945b7a10 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/UserService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserService.java @@ -22,15 +22,21 @@ package org.sonar.server.user; import org.sonar.api.ServerComponent; import org.sonar.core.permission.GlobalPermissions; +import org.sonar.server.user.index.UserDoc; +import org.sonar.server.user.index.UserIndex; import org.sonar.server.user.index.UserIndexer; +import javax.annotation.CheckForNull; + public class UserService implements ServerComponent { private final UserIndexer userIndexer; + private final UserIndex userIndex; private final UserCreator userCreator; - public UserService(UserIndexer userIndexer, UserCreator userCreator) { + public UserService(UserIndexer userIndexer, UserIndex userIndex, UserCreator userCreator) { this.userIndexer = userIndexer; + this.userIndex = userIndex; this.userCreator = userCreator; } @@ -40,6 +46,16 @@ public class UserService implements ServerComponent { userIndexer.index(); } + @CheckForNull + public UserDoc getByLogin(String login) { + return userIndex.getByLogin(login); + } + + @CheckForNull + public UserDoc getNullableByLogin(String login) { + return userIndex.getNullableByLogin(login); + } + public void index() { userIndexer.index(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java new file mode 100644 index 00000000000..c9beb4b210c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndex.java @@ -0,0 +1,59 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.user.index; + +import org.elasticsearch.action.get.GetRequestBuilder; +import org.elasticsearch.action.get.GetResponse; +import org.sonar.api.ServerComponent; +import org.sonar.server.es.EsClient; +import org.sonar.server.exceptions.NotFoundException; + +import javax.annotation.CheckForNull; + +public class UserIndex implements ServerComponent { + + private final EsClient esClient; + + public UserIndex(EsClient esClient) { + this.esClient = esClient; + } + + @CheckForNull + public UserDoc getNullableByLogin(String login) { + GetRequestBuilder request = esClient.prepareGet(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, login) + .setFetchSource(true) + .setRouting(login); + GetResponse response = request.get(); + if (response.isExists()) { + return new UserDoc(response.getSource()); + } + return null; + } + + public UserDoc getByLogin(String login) { + UserDoc userDoc = getNullableByLogin(login); + if (userDoc == null) { + throw new NotFoundException(String.format("User '%s' not found", login)); + } + return userDoc; + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java index 611b776a1ee..94bc1e1f317 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserIndexer.java @@ -21,6 +21,8 @@ package org.sonar.server.user.index; import org.apache.commons.dbutils.DbUtils; +import org.elasticsearch.action.get.GetRequestBuilder; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.update.UpdateRequest; import org.sonar.core.persistence.DbSession; import org.sonar.server.db.DbClient; @@ -28,6 +30,8 @@ import org.sonar.server.es.BaseIndexer; import org.sonar.server.es.BulkIndexer; import org.sonar.server.es.EsClient; +import javax.annotation.CheckForNull; + import java.sql.Connection; import java.util.Iterator; @@ -71,6 +75,21 @@ public class UserIndexer extends BaseIndexer { return maxUpdatedAt; } + @CheckForNull + public UserDoc getNullableByKey(String login) { + GetRequestBuilder request = esClient.prepareGet() + .setType(UserIndexDefinition.INDEX) + .setIndex(UserIndexDefinition.TYPE_USER) + .setId(login) + .setFetchSource(true) + .setRouting(login); + GetResponse response = request.get(); + if (response.isExists()) { + return new UserDoc(response.getSource()); + } + return null; + } + private UpdateRequest newUpsertRequest(UserDoc user) { return new UpdateRequest(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, user.login()) .doc(user.getFields()) diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java index b621e87ec9b..352607553c6 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/index/UserResultSetIterator.java @@ -36,7 +36,6 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Iterator; import java.util.List; import static com.google.common.collect.Lists.newArrayList; @@ -106,8 +105,8 @@ class UserResultSetIterator extends ResultSetIterator { reader = new StringReader(csv); csvParser = new CSVParser(reader, CSVFormat.DEFAULT); for (CSVRecord csvRecord : csvParser) { - for (Iterator iter = csvRecord.iterator(); iter.hasNext();) { - result.add(iter.next()); + for (String aCsvRecord : csvRecord) { + result.add(aCsvRecord); } } return result; diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java new file mode 100644 index 00000000000..aa0bd94ff0b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java @@ -0,0 +1,117 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.user.ws; + +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.RequestHandler; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.server.user.NewUser; +import org.sonar.server.user.ReactivationException; +import org.sonar.server.user.UserService; + +public class CreateAction implements RequestHandler { + + private static final String PARAM_LOGIN = "login"; + private static final String PARAM_PASSWORD = "password"; + private static final String PARAM_PASSWORD_CONFIRMATION = "password_confirmation"; + private static final String PARAM_NAME = "name"; + private static final String PARAM_EMAIL = "email"; + private static final String PARAM_PREVENT_REACTIVATION = "prevent_reactivation"; + + private final UserService userService; + + public CreateAction(UserService userService) { + this.userService = userService; + } + + void define(WebService.NewController controller) { + WebService.NewAction action = controller.createAction("create") + .setDescription("Create a user. Requires Administer System permission") + .setSince("3.7") + .setPost(true) + .setHandler(this); + + action.createParam(PARAM_LOGIN) + .setDescription("User login") + .setRequired(true) + .setExampleValue("myuser"); + + action.createParam(PARAM_PASSWORD) + .setDescription("User password") + .setRequired(true) + .setExampleValue("mypassword"); + + action.createParam(PARAM_PASSWORD_CONFIRMATION) + .setDescription("Must be the same value as \"password\"") + .setRequired(true) + .setExampleValue("mypassword"); + + action.createParam(PARAM_NAME) + .setDescription("User name") + .setRequired(true) + .setExampleValue("My Name"); + + action.createParam(PARAM_EMAIL) + .setDescription("User email") + .setExampleValue("myname@email.com"); + + action.createParam(PARAM_PREVENT_REACTIVATION) + .setDescription("If set to true and if the user has been removed, a status 409 will be returned") + .setDefaultValue(false) + .setBooleanPossibleValues(); + } + + @Override + public void handle(Request request, Response response) throws Exception { + NewUser newUser = NewUser.create() + .setLogin(request.mandatoryParam(PARAM_LOGIN)) + .setName(request.mandatoryParam(PARAM_NAME)) + .setEmail(request.param(PARAM_EMAIL)) + .setPassword(request.mandatoryParam(PARAM_PASSWORD)) + .setPasswordConfirmation(request.mandatoryParam(PARAM_PASSWORD_CONFIRMATION)) + .setPreventReactivation(request.mandatoryParamAsBoolean(PARAM_PREVENT_REACTIVATION)); + + try { + userService.create(newUser); + } catch (ReactivationException e) { + // write409(response, e.ruleKey()); + } + } + + // private void writeResponse(Response response, RuleKey ruleKey) { + // Rule rule = service.getNonNullByKey(ruleKey); + // JsonWriter json = response.newJsonWriter().beginObject().name("rule"); + // mapping.write(rule, json, null /* TODO replace by SearchOptions immutable constant */); + // json.endObject().close(); + // } + // + // private void write409(Response response, RuleKey ruleKey) { + // Rule rule = service.getNonNullByKey(ruleKey); + // + // Response.Stream stream = response.stream(); + // stream.setStatus(409); + // stream.setMediaType(MimeTypes.JSON); + // JsonWriter json = JsonWriter.of(new OutputStreamWriter(stream.output())).beginObject().name("rule"); + // mapping.write(rule, json, null /* TODO replace by SearchOptions immutable constant */); + // json.endObject().close(); + // } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWs.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWs.java index 518a39f5987..a5b7d1abdf2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWs.java +++ b/server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWs.java @@ -26,6 +26,12 @@ import org.sonar.api.server.ws.WebService; public class UsersWs implements WebService { + private final CreateAction createAction; + + public UsersWs(CreateAction createAction) { + this.createAction = createAction; + } + @Override public void define(Context context) { NewController controller = context.createController("api/users") @@ -33,7 +39,7 @@ public class UsersWs implements WebService { .setDescription("Users management"); defineSearchAction(controller); - defineCreateAction(controller); + createAction.define(controller); defineUpdateAction(controller); defineDeactivateAction(controller); @@ -59,40 +65,6 @@ public class UsersWs implements WebService { RailsHandler.addFormatParam(action); } - private void defineCreateAction(NewController controller) { - NewAction action = controller.createAction("create") - .setDescription("Create a user. Requires Administer System permission") - .setSince("3.7") - .setPost(true) - .setHandler(RailsHandler.INSTANCE); - - action.createParam("login") - .setDescription("User login") - .setRequired(true) - .setExampleValue("myuser"); - - action.createParam("password") - .setDescription("User password") - .setRequired(true) - .setExampleValue("mypassword"); - - action.createParam("password_confirmation") - .setDescription("Must be the same value as \"password\"") - .setRequired(true) - .setExampleValue("mypassword"); - - action.createParam("name") - .setDescription("User name") - .setRequired(true) - .setExampleValue("My Name"); - - action.createParam("email") - .setDescription("User email") - .setExampleValue("myname@email.com"); - - RailsHandler.addFormatParam(action); - } - private void defineUpdateAction(NewController controller) { NewAction action = controller.createAction("update") .setDescription("Update a user. Requires Administer System permission") diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/UserServiceMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/UserServiceMediumTest.java index c7a61cc6d92..164a45f8439 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/UserServiceMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/UserServiceMediumTest.java @@ -96,6 +96,42 @@ public class UserServiceMediumTest { .setScmAccounts(newArrayList("u1", "u_1"))); } + @Test + public void get_nullable_by_login() throws Exception { + MockUserSession.set().setLogin("john").setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + GroupDto userGroup = new GroupDto().setName(CoreProperties.CORE_DEFAULT_GROUP_DEFAULT_VALUE); + dbClient.groupDao().insert(session, userGroup); + session.commit(); + + service.create(NewUser.create() + .setLogin("user") + .setName("User") + .setEmail("user@mail.com") + .setPassword("password") + .setPasswordConfirmation("password") + .setScmAccounts(newArrayList("u1", "u_1"))); + + assertThat(service.getNullableByLogin("user")).isNotNull(); + } + + @Test + public void get_by_login() throws Exception { + MockUserSession.set().setLogin("john").setGlobalPermissions(GlobalPermissions.SYSTEM_ADMIN); + GroupDto userGroup = new GroupDto().setName(CoreProperties.CORE_DEFAULT_GROUP_DEFAULT_VALUE); + dbClient.groupDao().insert(session, userGroup); + session.commit(); + + service.create(NewUser.create() + .setLogin("user") + .setName("User") + .setEmail("user@mail.com") + .setPassword("password") + .setPasswordConfirmation("password") + .setScmAccounts(newArrayList("u1", "u_1"))); + + assertThat(service.getByLogin("user")).isNotNull(); + } + @Test public void index() throws Exception { UserDto userDto = new UserDto().setLogin("user").setEmail("user@mail.com").setCreatedAt(System.currentTimeMillis()).setUpdatedAt(System.currentTimeMillis()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java new file mode 100644 index 00000000000..8172bbae517 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexTest.java @@ -0,0 +1,87 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.user.index; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.server.es.EsTester; +import org.sonar.server.exceptions.NotFoundException; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class UserIndexTest { + + @Rule + public EsTester esTester = new EsTester().addDefinitions(new UserIndexDefinition(new Settings())); + + private UserIndex index; + + @Before + public void setUp() { + index = new UserIndex(esTester.client()); + } + + @Test + public void get_nullable_by_login() throws Exception { + esTester.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, this.getClass(), "get_nullable_by_login.json"); + + UserDoc userDoc = index.getNullableByLogin("user1"); + assertThat(userDoc).isNotNull(); + assertThat(userDoc.login()).isEqualTo("user1"); + assertThat(userDoc.name()).isEqualTo("User1"); + assertThat(userDoc.email()).isEqualTo("user1@mail.com"); + assertThat(userDoc.active()).isTrue(); + assertThat(userDoc.scmAccounts()).containsOnly("user_1", "u1"); + assertThat(userDoc.createdAt()).isEqualTo(1500000000000L); + assertThat(userDoc.updatedAt()).isEqualTo(1500000000000L); + + assertThat(index.getNullableByLogin("unknown")).isNull(); + } + + @Test + public void get_by_login() throws Exception { + esTester.putDocuments(UserIndexDefinition.INDEX, UserIndexDefinition.TYPE_USER, this.getClass(), "get_nullable_by_login.json"); + + UserDoc userDoc = index.getByLogin("user1"); + assertThat(userDoc).isNotNull(); + assertThat(userDoc.login()).isEqualTo("user1"); + assertThat(userDoc.name()).isEqualTo("User1"); + assertThat(userDoc.email()).isEqualTo("user1@mail.com"); + assertThat(userDoc.active()).isTrue(); + assertThat(userDoc.scmAccounts()).containsOnly("user_1", "u1"); + assertThat(userDoc.createdAt()).isEqualTo(1500000000000L); + assertThat(userDoc.updatedAt()).isEqualTo(1500000000000L); + } + + @Test + public void fail_to_get_by_login_on_unknown_user() throws Exception { + try { + index.getByLogin("unknown"); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(NotFoundException.class).hasMessage("User 'unknown' not found"); + } + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java index 194dedc5778..56b3d282b95 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/index/UserIndexerTest.java @@ -58,6 +58,7 @@ public class UserIndexerTest { List docs = esTester.getDocuments("users", "user", UserDoc.class); assertThat(docs).hasSize(1); UserDoc doc = docs.get(0); + assertThat(doc.login()).isEqualTo("user1"); assertThat(doc.name()).isEqualTo("User1"); assertThat(doc.email()).isEqualTo("user1@mail.com"); assertThat(doc.active()).isTrue(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java index 56c88b13966..55bed235cd6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsTest.java @@ -24,9 +24,11 @@ import org.junit.Before; import org.junit.Test; import org.sonar.api.server.ws.RailsHandler; import org.sonar.api.server.ws.WebService; +import org.sonar.server.user.UserService; import org.sonar.server.ws.WsTester; import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Mockito.mock; public class UsersWsTest { @@ -34,7 +36,7 @@ public class UsersWsTest { @Before public void setUp() throws Exception { - WsTester tester = new WsTester(new UsersWs()); + WsTester tester = new WsTester(new UsersWs(new CreateAction(mock(UserService.class)))); controller = tester.controller("api/users"); } @@ -61,7 +63,6 @@ public class UsersWsTest { WebService.Action action = controller.action("create"); assertThat(action).isNotNull(); assertThat(action.isPost()).isTrue(); - assertThat(action.handler()).isInstanceOf(RailsHandler.class); assertThat(action.params()).hasSize(6); } diff --git a/server/sonar-server/src/test/resources/org/sonar/server/user/index/UserIndexTest/get_nullable_by_login.json b/server/sonar-server/src/test/resources/org/sonar/server/user/index/UserIndexTest/get_nullable_by_login.json new file mode 100644 index 00000000000..7c29066e6cf --- /dev/null +++ b/server/sonar-server/src/test/resources/org/sonar/server/user/index/UserIndexTest/get_nullable_by_login.json @@ -0,0 +1,9 @@ +{ + "login": "user1", + "name": "User1", + "email": "user1@mail.com", + "active": true, + "scmAccounts": ["user_1", "u1"], + "createdAt": 1500000000000, + "updatedAt": 1500000000000 +} -- 2.39.5