diff --git a/Jenkinsfile b/Jenkinsfile index 59c370d..fe465f2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -60,19 +60,20 @@ pipeline { success { - script { - if ({env.BRANCH_NAME}.startsWith('pr-')) { - -// git url: "git@gogs.informatik.hs-fulda.de:SteffenN/Multi-Chess.git", -// credentialsId: 'jenkins_ssh_key', -// branch: (env.BRANCH_NAME) - - sh "git merge '${env.BRANCH_NAME}'" - sh "git commit -am 'Merged ${env.BRANCH_NAME} branch to master'" - sh "git push origin master" - } - } - +// script { +// +// sh "'${env.BRANCH_NAME}'" +// if (${env.BRANCH_NAME}.startsWith('pr-')) { +// +//// git url: "git@gogs.informatik.hs-fulda.de:SteffenN/Multi-Chess.git", +//// credentialsId: 'jenkins_ssh_key', +//// branch: (env.BRANCH_NAME) +// +// sh "git merge '${env.BRANCH_NAME}'" +// sh "git commit -am 'Merged ${env.BRANCH_NAME} branch to master'" +// sh "git push origin master" +// } +// } office365ConnectorSend color: 'good', message: "Build ${currentBuild.fullDisplayName} completed *successfully* (<${BUILD_URL}>).\n\n\n${CUSTOM_SCM_INFO}", webhookUrl: "https://outlook.office.com/webhook/97618564-835e-438e-a2a7-a77b21331e1e@22877e52-e9fd-410d-91a3-817d8ab89d63/JenkinsCI/fa736de2175649a891c2957f00532027/87d23462-1d0c-4378-b4e0-05c7d5546a25" diff --git a/Multi-Chess.xml b/Multi-Chess.xml new file mode 100644 index 0000000..60ceb42 --- /dev/null +++ b/Multi-Chess.xml @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4fc1d1f..1c6072a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,10 +10,6 @@ repositories { jcenter() } -dependencies { - testCompile group: 'junit', name: 'junit', version: '4.12' -} - subprojects { apply plugin: 'java' @@ -27,7 +23,15 @@ subprojects { compileOnly 'org.projectlombok:lombok:1.18.16' annotationProcessor 'org.projectlombok:lombok:1.18.16' + testImplementation('org.junit.jupiter:junit-jupiter:5.7.0') + testImplementation('org.mockito:mockito-core:3.7.0') + testImplementation('org.hamcrest:hamcrest-core:2.2') + testCompileOnly 'org.projectlombok:lombok:1.18.16' testAnnotationProcessor 'org.projectlombok:lombok:1.18.16' } + + test { + useJUnitPlatform() + } } diff --git a/fh.fd.ci.server/build.gradle b/fh.fd.ci.server/build.gradle index ad6ace5..2bf768e 100644 --- a/fh.fd.ci.server/build.gradle +++ b/fh.fd.ci.server/build.gradle @@ -2,4 +2,13 @@ dependencies { implementation project(':fh.fd.ci.shared') compile 'com.sparkjava:spark-core:2.9.3' + compile 'dev.morphia.morphia:core:1.5.3' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.5' + + task startServer(type: JavaExec){ + main = "de.fd.fh.ServerApp" + description = "Start server" + classpath = sourceSets.main.runtimeClasspath + } + } \ No newline at end of file diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/ServerApp.java b/fh.fd.ci.server/src/main/java/de/fd/fh/ServerApp.java index 384d27c..d275c81 100644 --- a/fh.fd.ci.server/src/main/java/de/fd/fh/ServerApp.java +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/ServerApp.java @@ -1,12 +1,57 @@ package de.fd.fh; +import de.fd.fh.server.access.AccessContextEventListener; +import de.fd.fh.server.access.AccessRepository; +import de.fd.fh.server.access.AccessService; +import de.fd.fh.server.access.web.AccessController; +import de.fd.fh.server.user.UserContextEventListener; +import de.fd.fh.server.user.web.UserController; +import de.fd.fh.server.user.UserRepository; +import de.fd.fh.server.user.UserService; + + +import java.util.HashSet; +import java.util.Observable; +import java.util.Observer; +import java.util.Set; + import static spark.Spark.*; public class ServerApp { + private static AccessRepository accessRepository; + private static UserRepository userRepository; + + private static final Set listeners = new HashSet<>(); public static void main(String[] args) { + initRepositories(); + + initListeners(); + + new AccessController((AccessService) addListeners(new AccessService(accessRepository))); + new UserController((UserService) addListeners(new UserService(userRepository))); + get("/hello", (req, res) -> "Hello World"); + + } + + private static Object addListeners(Observable service) + { + listeners.forEach(service::addObserver); + + return service; + } + + private static void initListeners() + { + listeners.add(new AccessContextEventListener(accessRepository)); + listeners.add(new UserContextEventListener(userRepository)); + } + + private static void initRepositories() { + accessRepository = new AccessRepository(); + userRepository = new UserRepository(); } } diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/Access.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/Access.java new file mode 100644 index 0000000..d3ab03b --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/Access.java @@ -0,0 +1,46 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.UserId; +import dev.morphia.annotations.Embedded; +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity("login") +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class Access +{ + @Id + private String _id; + + private String name; + + private String password; + + @Embedded + private UserId userId; + + @Embedded + private AccessToken token; + + private Role role; + + void removeToken() + { + this.token = null; + } + + void setToken(final AccessToken token) + { + this.token = token; + } + + void updatePassword(final String newPassword) + { + this.password = newPassword; + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessContextEventListener.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessContextEventListener.java new file mode 100644 index 0000000..4920e07 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessContextEventListener.java @@ -0,0 +1,28 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.events.ChangePasswordEvent; +import lombok.RequiredArgsConstructor; + +import java.util.Observable; +import java.util.Observer; + +@RequiredArgsConstructor +public class AccessContextEventListener implements Observer +{ + private final AccessRepository accessRepository; + + @Override + public void update(Observable observable, Object o) + { + if(o instanceof ChangePasswordEvent) + { + final ChangePasswordEvent event = (ChangePasswordEvent) o; + + final Access access = accessRepository.findByUserId(event.getUserId()); + + access.updatePassword(event.getNewPassword()); + + accessRepository.save(access); + } + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessRepository.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessRepository.java new file mode 100644 index 0000000..7149cf6 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessRepository.java @@ -0,0 +1,58 @@ +package de.fd.fh.server.access; + +import com.mongodb.MongoClient; +import com.mongodb.WriteResult; +import de.fd.fh.server.user.UserId; +import dev.morphia.Datastore; +import dev.morphia.Key; +import dev.morphia.Morphia; + +public class AccessRepository +{ + private final Datastore datastore; + + public AccessRepository() + { + System.out.println("AccessRepo"); + final Morphia morphia = new Morphia(); + + morphia.mapPackage("de.fd.fh.server.access"); + + this.datastore = morphia.createDatastore(new MongoClient(), "smartwarfare"); + datastore.ensureIndexes(); + } + + AccessRepository(Datastore datastore) + { + this.datastore = datastore; + } + + public Key save(final Access access) + { + return datastore.save(access); + } + + Access findByUserName(final String name) + { + return datastore.createQuery(Access.class) + .field("name").equal(name).first(); + } + + Access findByToken(final String token) + { + return datastore.createQuery(Access.class) + .field("token.token").equal(token).first(); + } + + Access findByUserId(final UserId userId) + { + return datastore.createQuery(Access.class) + .field("userId.identifier").equal(userId.getIdentifier()).first(); + } + + WriteResult deleteLoginByUserId(final UserId userId) + { + return datastore.delete(datastore.createQuery(Access.class) + .field("userId.identifier").equal(userId.getIdentifier()).first()); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessService.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessService.java new file mode 100644 index 0000000..752d321 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessService.java @@ -0,0 +1,152 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.access.events.AccountCreatedEvent; +import de.fd.fh.server.access.events.AccountDeletedEvent; +import de.fd.fh.server.user.UserId; +import de.fd.fh.shared.network.messages.LoginRequest; +import de.fd.fh.shared.network.messages.RegistrateRequest; +import lombok.RequiredArgsConstructor; +import org.bson.types.ObjectId; + +import java.util.Base64; +import java.util.Observable; + +import static spark.Spark.halt; + +@RequiredArgsConstructor +public class AccessService extends Observable +{ + private final AccessRepository accessRepository; + + public AccessToken before(final String path, final String token) { + System.out.println("Pfad: " + path); + if (!(path.equals("/accounts/login") + || path.equals("/accounts/registrate") + )) + { + final AccessToken accessToken = authenticate(token); + + if (accessToken == null) + { + halt(401); + } + return accessToken; + } + return null; + } + + private AccessToken authenticate(final String bearerToken) + { + return accessRepository.findByToken(bearerToken.substring("Bearer ".length())).getToken(); + } + + public boolean createPlayer(RegistrateRequest message) + { + System.out.println("createPlayer: " + message); + + if (userNameDoesNotExist(message.getUserName())) + { + System.out.println("Name does exist."); + return false; + } + + final Access access = new Access( + new ObjectId().toHexString(), + message.getUserName(), + message.getPassword(), + UserId.random(), + null, + Role.USER + ); + + accessRepository.save(access); + + setChanged(); + notifyObservers(new AccountCreatedEvent(access.getName(), + access.getUserId())); + + System.out.println("DBLogin: " + access); + + return true; + } + + private boolean userNameDoesNotExist(final String name) + { + final Access user = accessRepository.findByUserName(name); + return user != null; + } + + public boolean logout(final String header) + { + try + { + System.out.println("logout " + header); + + final Access access = accessRepository.findByToken(header.substring("Bearer ".length())); + + access.removeToken(); + + accessRepository.save(access); + + return true; + } catch (Exception e) + { + e.printStackTrace(); + + return false; + } + } + + public LoginRequest authorization(final String header) + { + System.out.println("authorization"); + final String auth = header.substring("Basic ".length()); + + try + { + byte[] message = Base64.getDecoder().decode(auth); + + String messageStr = new String(message); + String[] user_password = messageStr.split(":"); + + final Access access = accessRepository.findByUserName(user_password[0]); + + System.out.println(access.getName()); + if (user_password[1].equals(access.getPassword())) + { + access.setToken(AccessToken.of(access)); + accessRepository.save(access); + + final LoginRequest loginRequest = new LoginRequest(); + loginRequest.setUserId(access.getUserId().getIdentifier()); + loginRequest.setToken(access.getToken().getToken()); + loginRequest.setName(access.getName()); + + return loginRequest; + } + + return null; + } catch (Exception e) + { + e.printStackTrace(); + return null; + } + } + + public boolean deleteAccount(final UserId userId, final AccessToken token) + { + if (!token.getUserId().getIdentifier() + .equals(userId.getIdentifier())) + { + return false; + } + if (accessRepository.deleteLoginByUserId(userId).wasAcknowledged()) + { + setChanged(); + notifyObservers(new AccountDeletedEvent(userId)); + + return true; + } + return false; + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessToken.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessToken.java new file mode 100644 index 0000000..e65bdab --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/AccessToken.java @@ -0,0 +1,62 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.UserId; +import dev.morphia.annotations.Embedded; +import dev.morphia.annotations.PrePersist; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Random; + +@Embedded +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class AccessToken +{ + private String token; + + private LocalDateTime createdDate; + + private Role role; + + @Embedded + private UserId userId; + + static AccessToken of(final Access access) + { + return new AccessToken( + generateToken(), + LocalDateTime.now(), + access.getRole(), + access.getUserId() + ); + } + + @PrePersist + void prePersist() + { + this.createdDate = LocalDateTime.now(); + } + + private static String generateToken() + { + final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + final String lower = upper.toLowerCase(); + final String numbers = "0123456789"; + final String alphabet = upper + lower + numbers; + + System.out.println("generate Security Token."); + + Random random = new Random(); + StringBuilder generatedString = new StringBuilder(); + for (int i = 0; i < 64; i++) { + generatedString.append(alphabet.charAt(random.nextInt(alphabet.length()))); + } + + System.out.println("Token: " + generatedString); + return generatedString.toString(); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/Role.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/Role.java new file mode 100644 index 0000000..66db360 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/Role.java @@ -0,0 +1,6 @@ +package de.fd.fh.server.access; + +public enum Role +{ + ADMIN, USER +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountCreatedEvent.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountCreatedEvent.java new file mode 100644 index 0000000..c5e1530 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountCreatedEvent.java @@ -0,0 +1,13 @@ +package de.fd.fh.server.access.events; + +import de.fd.fh.server.user.UserId; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class AccountCreatedEvent +{ + private final String name; + private final UserId userId; +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountDeletedEvent.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountDeletedEvent.java new file mode 100644 index 0000000..7667bcb --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/events/AccountDeletedEvent.java @@ -0,0 +1,12 @@ +package de.fd.fh.server.access.events; + +import de.fd.fh.server.user.UserId; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class AccountDeletedEvent +{ + private final UserId userId; +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/web/AccessController.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/web/AccessController.java new file mode 100644 index 0000000..3e32964 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/access/web/AccessController.java @@ -0,0 +1,104 @@ +package de.fd.fh.server.access.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fd.fh.server.access.AccessService; +import de.fd.fh.server.access.AccessToken; +import de.fd.fh.server.user.UserId; +import de.fd.fh.shared.Utils; +import de.fd.fh.shared.network.messages.LoginRequest; +import de.fd.fh.shared.network.messages.RegistrateRequest; + +import static spark.Spark.*; + +public class AccessController +{ + private final ObjectMapper objectMapper = new ObjectMapper(); + + public AccessController(final AccessService service) + { + before("/*", + (req, res) -> + { + final String path = req.pathInfo(); + final String token = req.headers(Utils.AUTHENTICATION_HEADER); + + final AccessToken accessToken = service.before(path, token); + + req.session().attribute("userId", + accessToken); + }); + + post("/accounts/registrate", + (request, response) -> + { + final RegistrateRequest message = + objectMapper.readValue(request.body(), RegistrateRequest.class); + + if (service.createPlayer(message)) + { + response.status(201); + } + else + { + response.status(400); + } + return response; + } + ); + + post("/accounts/login", + (request, response) -> + { + final String header = request.headers(Utils.AUTHENTICATION_HEADER); + + final LoginRequest login = service.authorization(header); + + if (login == null) + { + response.status(401); + } + else + { + response.status(200); + response.type("application/json"); + response.body(objectMapper.writeValueAsString(login)); + } + return response; + }); + + post("/accounts/logout", + (request, response) -> + { + final String token = request.headers(Utils.AUTHENTICATION_HEADER); + + if (service.logout(token)) + { + response.status(200); + } + else + { + response.status(400); + } + + return response; + }); + + delete("/accounts/:player_id", + (request, response) -> + { + final UserId userId = UserId.of(request.params(":player_id")); + final AccessToken token = request.session().attribute("userId"); + + if (service.deleteAccount(userId, token)) + { + response.status(200); + } + else + { + response.status(400); + } + + return response; + }); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/User.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/User.java new file mode 100644 index 0000000..ef18257 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/User.java @@ -0,0 +1,26 @@ +package de.fd.fh.server.user; + +import lombok.*; + +@AllArgsConstructor +@Getter +public class User { + + private final UserId id; + + private String name; + + public static User of(String name) + { + return new User(null, name); + } + + public void rename(String name) + { + if (name == null) + { + return; + } + this.name = name; + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserContextEventListener.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserContextEventListener.java new file mode 100644 index 0000000..c9fe1a5 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserContextEventListener.java @@ -0,0 +1,43 @@ +package de.fd.fh.server.user; + +import de.fd.fh.server.access.events.AccountCreatedEvent; +import de.fd.fh.server.access.events.AccountDeletedEvent; +import lombok.RequiredArgsConstructor; + +import java.util.Observable; +import java.util.Observer; + +@RequiredArgsConstructor +public class UserContextEventListener implements Observer +{ + private final UserRepository userRepository; + + @Override + public void update( + final Observable observable, + final Object event) + { + System.out.println("UserContextEventListener " + event); + if (event instanceof AccountCreatedEvent) { + handleAccountCreatedEvent((AccountCreatedEvent) event); + } + if (event instanceof AccountDeletedEvent) { + handleAccountDeletedEvent((AccountDeletedEvent) event); + } + } + + private void handleAccountDeletedEvent(final AccountDeletedEvent event) + { + userRepository.deleteUserById(event.getUserId()); + } + + private void handleAccountCreatedEvent(final AccountCreatedEvent event) + { + System.out.println("handleAccountCreatedEvent " + event); + final User user = new User(event.getUserId(), event.getName()); + System.out.println("User: " + user); + + userRepository.save(user); + System.out.println("UserId: " + user.getId()); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserId.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserId.java new file mode 100644 index 0000000..d978069 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserId.java @@ -0,0 +1,25 @@ +package de.fd.fh.server.user; + +import dev.morphia.annotations.Embedded; +import lombok.*; +import org.bson.types.ObjectId; + +@Getter +@Embedded +@NoArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode(of = {"identifier"}) +public class UserId +{ + private String identifier; + + public static UserId of(final String identifier) + { + return new UserId(identifier); + } + + public static UserId random() + { + return new UserId(new ObjectId().toHexString()); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserRepository.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserRepository.java new file mode 100644 index 0000000..dcb1316 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserRepository.java @@ -0,0 +1,52 @@ +package de.fd.fh.server.user; + +import com.mongodb.MongoClient; +import com.mongodb.WriteResult; +import dev.morphia.Datastore; +import dev.morphia.Key; +import dev.morphia.Morphia; + +public class UserRepository { + + private final Datastore datastore; + + public UserRepository() + { + System.out.println("UserRepo"); + final Morphia morphia = new Morphia(); + + morphia.mapPackage("de.fd.fh.server.user"); + + this.datastore = morphia.createDatastore(new MongoClient(), "smartwarfare"); + datastore.ensureIndexes(); + } + + UserRepository(Datastore datastore) + { + this.datastore = datastore; + } + + public Key save(final User user) + { + return datastore.save(user); + } + + public User findUserById(final UserId userId) + { + return datastore.createQuery(User.class) + .field("_id.identifier").equal(userId.getIdentifier()) + .first(); + } + + public User findUserByName(final String name) + { + return datastore.createQuery(User.class) + .field("name").equal(name) + .first(); + } + + WriteResult deleteUserById(final UserId userId) + { + return datastore.delete(findUserById(userId)); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserService.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserService.java new file mode 100644 index 0000000..268e695 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/UserService.java @@ -0,0 +1,48 @@ +package de.fd.fh.server.user; + +import de.fd.fh.server.user.events.ChangePasswordEvent; +import de.fd.fh.server.user.web.ChangeUserRequest; +import de.fd.fh.server.user.web.UserRequest; +import lombok.RequiredArgsConstructor; + +import java.util.Observable; + +@RequiredArgsConstructor +public class UserService extends Observable +{ + private final UserRepository userRepository; + + public User changePlayer(final UserId userId, final ChangeUserRequest message) + { + System.out.println("changePlayer: " + message); + + User user = userRepository.findUserById(userId); + + if (message.getPassword() != null) + { + setChanged(); + notifyObservers(new ChangePasswordEvent(userId, message.getPassword())); + } + + userRepository.save(user); + + return userRepository.findUserById(userId); + } + + public User getPlayer(final UserId id) + { + return userRepository.findUserById(id); + } + + public UserRequest getSmallPlayer(final UserId userId) + { + final User user = userRepository.findUserById(userId); + + if(user == null) + { + return null; + } + + return new UserRequest(user.getId().getIdentifier(), user.getName()); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/events/ChangePasswordEvent.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/events/ChangePasswordEvent.java new file mode 100644 index 0000000..bc15e48 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/events/ChangePasswordEvent.java @@ -0,0 +1,18 @@ +package de.fd.fh.server.user.events; + +import de.fd.fh.server.user.UserId; +import lombok.Getter; + +@Getter +public class ChangePasswordEvent +{ + private final String newPassword; + + private final UserId userId; + + public ChangePasswordEvent(final UserId userId, final String password) + { + this.newPassword = password; + this.userId = userId; + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/ChangeUserRequest.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/ChangeUserRequest.java new file mode 100644 index 0000000..82fbe76 --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/ChangeUserRequest.java @@ -0,0 +1,13 @@ +package de.fd.fh.server.user.web; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ChangeUserRequest +{ + private final String name; + + private final String password; +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/UserController.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/UserController.java new file mode 100644 index 0000000..67a932d --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/UserController.java @@ -0,0 +1,89 @@ +package de.fd.fh.server.user.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fd.fh.server.access.AccessToken; +import de.fd.fh.server.user.User; +import de.fd.fh.server.user.UserId; +import de.fd.fh.server.user.UserService; + +import static spark.Spark.get; +import static spark.Spark.post; + +public class UserController +{ + private ObjectMapper objectMapper = new ObjectMapper(); + + public UserController(final UserService service) + { + post("/users", + ((request, response) -> + { + final UserId userId = + ((AccessToken) request.session() + .attribute("userId")) + .getUserId(); + + final ChangeUserRequest message = objectMapper.readValue(request.body(), + ChangeUserRequest.class); + + final User user = service.changePlayer( + userId, + message); + + if (user == null) + { + response.status(400); + } + else + { + response.status(200); + response.type("application/json"); + + return objectMapper.writeValueAsString(user); + } + + return response; + } + )); + + get("/users", + (request, response) -> + { + final UserId userId = + ((AccessToken) request.session() + .attribute("userId")) + .getUserId(); + + final User user = service.getPlayer(userId); + + if (user == null) + { + response.status(400); + } + else + { + response.status(200); + response.type("application/json"); + + return objectMapper.writeValueAsString(user); + } + + return response; + } + ); + + get("/users/:user_id", + (request, response) -> + { + final UserId userId = UserId.of(request.params(":user_id")); + final UserRequest user = service.getSmallPlayer(userId); + + if (user == null) + { + response.status(404); + } + response.body(objectMapper.writeValueAsString(user)); + return response; + }); + } +} diff --git a/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/UserRequest.java b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/UserRequest.java new file mode 100644 index 0000000..981861c --- /dev/null +++ b/fh.fd.ci.server/src/main/java/de/fd/fh/server/user/web/UserRequest.java @@ -0,0 +1,13 @@ +package de.fd.fh.server.user.web; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class UserRequest +{ + private final String id; + + private final String name; +} diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessContextEventListenerTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessContextEventListenerTest.java new file mode 100644 index 0000000..e970141 --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessContextEventListenerTest.java @@ -0,0 +1,36 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.UserId; +import de.fd.fh.server.user.UserRepository; +import de.fd.fh.server.user.UserService; +import de.fd.fh.server.user.events.ChangePasswordEvent; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.*; + +class AccessContextEventListenerTest +{ + @Test + void given_changePasswordEvent_when_passwordChanged_should_changePassword() + { + final ChangePasswordEvent event = new ChangePasswordEvent(UserId.of("12345"), "newPwd"); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByUserId(any(UserId.class))) + .thenReturn(new Access()); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Access.class); + + new AccessContextEventListener(repository).update(null, event); + verify(repository).save(captor.capture()); + + assertEquals("newPwd", captor.getValue().getPassword(), "Have to be the new password"); + then(repository).should().findByUserId(any(UserId.class)); + then(repository).should().save(any(Access.class)); + then(repository).shouldHaveNoMoreInteractions(); + } +} \ No newline at end of file diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessRepositoryTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessRepositoryTest.java new file mode 100644 index 0000000..488d332 --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessRepositoryTest.java @@ -0,0 +1,42 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.UserId; +import dev.morphia.Datastore; +import dev.morphia.Key; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AccessRepositoryTest +{ + @Mock + private Datastore datastore; + + @BeforeEach + public void before() + { + datastore = mock(Datastore.class); + } + + @Test + void given_newUser_when_saveUser_should_storeUserInDatabase() + { + when(datastore.save(any(Access.class))) + .thenReturn(new Key<>(Access.class, "collection", "id")); + + final Access access = new Access("testId", "testName", "testPwd", UserId.of("userId"), + null, Role.USER); + + final Key result = new AccessRepository(datastore).save(access); + + assertThat("Key is null", result, notNullValue()); + then(datastore).should().save(any(Access.class)); + } +} \ No newline at end of file diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessServiceTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessServiceTest.java new file mode 100644 index 0000000..073a84a --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessServiceTest.java @@ -0,0 +1,245 @@ +package de.fd.fh.server.access; + +import com.mongodb.WriteResult; +import de.fd.fh.server.user.UserId; +import de.fd.fh.shared.network.messages.LoginRequest; +import de.fd.fh.shared.network.messages.RegistrateRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import spark.HaltException; + +import java.time.LocalDateTime; +import java.util.Base64; +import java.util.Observable; +import java.util.Observer; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.*; + +class AccessServiceTest implements Observer +{ + private Object event; + + @BeforeEach + void before() + { + this.event = null; + } + + @Test + void given_authenticatedUser_when_serverAuthenticateUser_should_authenticateUser() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + new AccessToken( + "testToken", + LocalDateTime.now(), + Role.USER, + UserId.of("12345") + ), + Role.USER); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByToken(any())) + .thenReturn(access); + final String path = "/test/path"; + final String token = "testToken"; + + final AccessToken result = new AccessService(repository).before(path, token); + + assertThat("User is not authenticated", result, notNullValue()); + then(repository).should().findByToken(any()); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_notAuthenticatedUser_when_serverAuthenticateUser_should_denyUser() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + null, + Role.USER); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByToken(any())) + .thenReturn(access); + final String path = "/test/path"; + final String token = "testToken"; + + assertThrows(HaltException.class, () ->new AccessService(repository).before(path, token)); + + then(repository).should().findByToken(any()); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_newUser_when_createUser_should_storeNewUser() + { + final RegistrateRequest request = + RegistrateRequest.of("testUser", "testPwd"); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByUserName(any())) + .thenReturn(null); + ArgumentCaptor accessCaptor = ArgumentCaptor.forClass(Access.class); + + final AccessService service = new AccessService(repository); + service.addObserver(this); + + final boolean result = service.createPlayer(request); + + assertTrue(result); + assertThat("No event thrown", this.event, notNullValue()); + + verify(repository).save(accessCaptor.capture()); + final Access createdAccess = accessCaptor.getValue(); + + assertNotNull(createdAccess, "No Access created"); + assertNotNull(createdAccess.get_id(), "No Id created"); + assertNotNull(createdAccess.getUserId(), "No UserId created"); + assertEquals("testUser", createdAccess.getName(), "Wrong Username"); + assertEquals("testPwd", createdAccess.getPassword(), "Wrong Password"); + assertEquals(Role.USER.name(), createdAccess.getRole().name(), "Should be USER"); + assertNull(createdAccess.getToken(), "User should not be logged in"); + + then(repository).should().findByUserName(any()); + then(repository).should().save(any(Access.class)); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_loggedInUser_when_logout_should_logoutUser() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + null, + Role.USER + ); + final AccessToken token = AccessToken.of(access); + access.setToken(token); + + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByToken(any())) + .thenReturn(access); + ArgumentCaptor accessCaptor = ArgumentCaptor.forClass(Access.class); + + final AccessService service = new AccessService(repository); + + final boolean result = service.logout("testToken"); + + assertTrue(result); + + verify(repository).save(accessCaptor.capture()); + final Access createdAccess = accessCaptor.getValue(); + + assertNotNull(createdAccess, "No Access created"); + assertNotNull(createdAccess.get_id(), "No Id created"); + assertNotNull(createdAccess.getUserId(), "No UserId created"); + assertEquals("testName", createdAccess.getName(), "Wrong Username"); + assertEquals("testPwd", createdAccess.getPassword(), "Wrong Password"); + assertEquals(Role.USER.name(), createdAccess.getRole().name(), "Should be USER"); + assertNull(createdAccess.getToken(), "User should logged out"); + + then(repository).should().findByToken(any()); + then(repository).should().save(any(Access.class)); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_storedUser_when_loginUser_should_returnLoginRequest() + { + final byte[] message = Base64.getEncoder().encode("testName:testPassword".getBytes()); + final String header = "Basic " + new String(message); + final Access access = new Access( + "testId", + "testName", + "testPassword", + UserId.of("12345"), + null, + Role.USER + ); + final AccessRepository repository = mock(AccessRepository.class); + when(repository.findByUserName(any())) + .thenReturn(access); + + final LoginRequest result = new AccessService(repository).authorization(header); + + assertNotNull(result); + assertEquals(result.getName(), "testName", "Wrong UserName"); + assertEquals(result.getUserId(), "12345", "Wrong Password"); + assertNotNull(result.getToken(), "Not logged in"); + } + + @Test + void given_storedUserWithWrongPassword_when_loginUser_should_returnAccessDeny() + { + final byte[] message = Base64.getEncoder().encode("testName:testPassword".getBytes()); + final String header = "Basic " + new String(message); + final AccessRepository repository = mock(AccessRepository.class); + + final LoginRequest result = new AccessService(repository).authorization(header); + + assertNull(result, "Return LoginRequest but wrong permissions"); + } + + @Test + void given_storedUser_when_deleteUser_should_deleteUser() + { + final UserId userId = UserId.of("12345"); + final AccessToken token = new AccessToken(null, null, null, UserId.of("12345")); + final AccessRepository repository = mock(AccessRepository.class); + when(repository.deleteLoginByUserId(any(UserId.class))) + .thenReturn(new WriteResult(1, false, null)); + + final AccessService service = new AccessService(repository); + service.addObserver(this); + + final boolean result = service.deleteAccount(userId, token); + + assertTrue(result); + assertNotNull(event); + + then(repository).should().deleteLoginByUserId(any(UserId.class)); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_storedUser_when_deleteUserWithWrongPermission_should_doNothing() + { + final UserId userId = UserId.of("12345"); + final AccessToken token = new AccessToken(null, null, null, UserId.of("98765")); + final AccessRepository repository = mock(AccessRepository.class); + when(repository.deleteLoginByUserId(any(UserId.class))) + .thenReturn(new WriteResult(1, false, null)); + + final AccessService service = new AccessService(repository); + service.addObserver(this); + + final boolean result = service.deleteAccount(userId, token); + + assertFalse(result); + assertNull(event); + + then(repository).shouldHaveNoInteractions(); + } + + @Override + public void update(Observable o, Object arg) + { + this.event = arg; + } +} \ No newline at end of file diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessTokenTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessTokenTest.java new file mode 100644 index 0000000..1872e62 --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/access/AccessTokenTest.java @@ -0,0 +1,31 @@ +package de.fd.fh.server.access; + +import de.fd.fh.server.user.UserId; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.*; + +class AccessTokenTest +{ + @Test + void given_accessData_when_createAccessToken_should_createGeneratedToken() + { + final Access access = new Access( + "testId", + "testName", + "testPwd", + UserId.of("12345"), + null, + Role.USER); + + final AccessToken result = AccessToken.of(access); + + assertNotNull(result.getCreatedDate()); + assertNotNull(result.getToken()); + assertThat(result.getToken().length(), equalTo(64)); + assertEquals(result.getRole(), Role.USER); + assertEquals(result.getUserId(), UserId.of("12345")); + } +} \ No newline at end of file diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserContextEventListenerTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserContextEventListenerTest.java new file mode 100644 index 0000000..294c9d7 --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserContextEventListenerTest.java @@ -0,0 +1,51 @@ +package de.fd.fh.server.user; + +import de.fd.fh.server.access.events.AccountCreatedEvent; +import de.fd.fh.server.access.events.AccountDeletedEvent; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class UserContextEventListenerTest +{ + @Test + void given_accountCreatedEvent_when_accountWasCreated_should_createUser() + { + final AccountCreatedEvent event = new AccountCreatedEvent("testName", UserId.of("12345")); + final UserRepository repository = mock(UserRepository.class); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + + new UserContextEventListener(repository).update(null, event); + verify(repository).save(captor.capture()); + + assertNotNull(captor.getValue()); + assertEquals("testName", captor.getValue().getName(), "Should have the correct name"); + assertEquals("12345", captor.getValue().getId().getIdentifier(), "Should have the correct userId"); + then(repository).should().save(any()); + then(repository).shouldHaveNoMoreInteractions(); + } + + @Test + void given_accountDeletedEvent_when_accountWasDeleted_should_deleteUser() + { + final AccountDeletedEvent event = new AccountDeletedEvent(UserId.of("12345")); + final UserRepository repository = mock(UserRepository.class); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(UserId.class); + + new UserContextEventListener(repository).update(null, event); + verify(repository).deleteUserById(captor.capture()); + + assertNotNull(captor.getValue()); + assertEquals("12345", captor.getValue().getIdentifier(), "No correct userId"); + then(repository).should().deleteUserById(any(UserId.class)); + then(repository).shouldHaveNoMoreInteractions(); + } +} \ No newline at end of file diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserRepositoryTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserRepositoryTest.java new file mode 100644 index 0000000..a551286 --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserRepositoryTest.java @@ -0,0 +1,40 @@ +package de.fd.fh.server.user; + +import dev.morphia.Datastore; +import dev.morphia.Key; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class UserRepositoryTest +{ + @Mock + private Datastore datastore; + + @BeforeEach + public void before() + { + datastore = mock(Datastore.class); + } + + @Test + void given_newUser_when_saveUser_should_storeUserInDatabase() + { + when(datastore.save(any(User.class))) + .thenReturn(new Key<>(User.class, "collection", "id")); + + final User access = new User(UserId.of("userId"), "testName"); + + final Key result = new UserRepository(datastore).save(access); + + assertThat("Key is null", result, notNullValue()); + then(datastore).should().save(any(User.class)); + } +} \ No newline at end of file diff --git a/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserServiceTest.java b/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserServiceTest.java new file mode 100644 index 0000000..1c3bcbf --- /dev/null +++ b/fh.fd.ci.server/src/test/java/de/fd/fh/server/user/UserServiceTest.java @@ -0,0 +1,84 @@ +package de.fd.fh.server.user; + +import de.fd.fh.server.user.web.ChangeUserRequest; +import de.fd.fh.server.user.web.UserRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Observable; +import java.util.Observer; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class UserServiceTest implements Observer +{ + private Object event; + + @BeforeEach + void before() + { + event = null; + } + + @Test + void given_storedUser_when_changePassword_should_changePassword() + { + final User user = + User.of("testName"); + final ChangeUserRequest request = + new ChangeUserRequest("testName", "newPassword"); + final UserRepository repository = mock(UserRepository.class); + when(repository.findUserById(any(UserId.class))) + .thenReturn(user); + + final UserService service = new UserService(repository); + service.addObserver(this); + final ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + + service.changePlayer(UserId.of("12345"), request); + verify(repository).save(captor.capture()); + + assertNotNull(captor.getValue(), "Should be saved"); + assertNotNull(event); + } + + @Test + void given_storedUser_when_getPlayer_should_returnPlayer() + { + final User user = + User.of("testName"); + final UserRepository repository = mock(UserRepository.class); + when(repository.findUserById(any(UserId.class))) + .thenReturn(user); + + final User result = new UserService(repository).getPlayer(UserId.of("12345")); + + assertNotNull(result); + } + + @Test + void given_storedUser_when_getSmallPlayer_should_returnSmallPlayer() + { + final User user = + new User(UserId.of("12345"), "testName"); + final UserRepository repository = mock(UserRepository.class); + when(repository.findUserById(any(UserId.class))) + .thenReturn(user); + + final UserRequest result = new UserService(repository).getSmallPlayer(UserId.of("12345")); + + assertNotNull(result); + assertEquals("12345", result.getId(), "Wrong UserId"); + assertEquals("testName", result.getName(), "Wrong Name"); + + } + + @Override + public void update(Observable o, Object arg) + { + event = arg; + } +} \ No newline at end of file diff --git a/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/Utils.java b/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/Utils.java new file mode 100644 index 0000000..32c326d --- /dev/null +++ b/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/Utils.java @@ -0,0 +1,6 @@ +package de.fd.fh.shared; + +public class Utils +{ + public static final String AUTHENTICATION_HEADER = "Authorization"; +} diff --git a/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/network/messages/LoginRequest.java b/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/network/messages/LoginRequest.java new file mode 100644 index 0000000..4e1c38c --- /dev/null +++ b/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/network/messages/LoginRequest.java @@ -0,0 +1,11 @@ +package de.fd.fh.shared.network.messages; + +import lombok.Data; + +@Data +public class LoginRequest +{ + private String name; + private String userId; + private String token; +} diff --git a/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/network/messages/RegistrateRequest.java b/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/network/messages/RegistrateRequest.java new file mode 100644 index 0000000..b3c08d4 --- /dev/null +++ b/fh.fd.ci.shared/src/main/java/de/fd/fh/shared/network/messages/RegistrateRequest.java @@ -0,0 +1,12 @@ +package de.fd.fh.shared.network.messages; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor(staticName = "of") +public class RegistrateRequest +{ + private String userName; + private String password; +}