From 80073e69a27bc92662c607a2b21cb4b86f148083 Mon Sep 17 00:00:00 2001
From: alaskowski <alaskowski@ethz.ch>
Date: Tue, 7 Mar 2023 16:57:45 +0100
Subject: [PATCH] SSDM-13251: Implemented login, isSessionActive, logout
 methods for client. Added tests

---
 api-data-store-server-java/build.gradle       |   4 +-
 .../ethz/sis/afsclient/client/AfsClient.java  | 148 ++++++++++++------
 .../sis/afsclient/client/AfsClientTest.java   |  90 +++++++----
 .../sis/afsclient/client/DummyHttpServer.java |   6 +
 server-data-store/build.gradle                |   4 +-
 .../afsserver/http/impl/NettyHttpHandler.java |  70 ++++++---
 .../afsserver/http/impl/NettyHttpServer.java  |  68 +++++---
 .../server/impl/ApiServerAdapter.java         | 135 ++++++++++------
 .../ch/ethz/sis/afsserver/ApiClientTest.java  |  74 ++++++++-
 9 files changed, 419 insertions(+), 180 deletions(-)

diff --git a/api-data-store-server-java/build.gradle b/api-data-store-server-java/build.gradle
index 340bf18c3b8..c770d54e370 100644
--- a/api-data-store-server-java/build.gradle
+++ b/api-data-store-server-java/build.gradle
@@ -23,6 +23,6 @@ dependencies {
     annotationProcessor 'lombok:lombok:1.18.22'
     implementation project(':lib-json'),
             'lombok:lombok:1.18.22'
-    testImplementation 'junit:junit:4.10'
-    testRuntimeOnly 'hamcrest:hamcrest-core:1.3'
+    testImplementation 'junit:junit:4.10',
+            'hamcrest:hamcrest-core:1.3'
 }
diff --git a/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java b/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java
index 2059a51fe55..63dc36dbcbe 100644
--- a/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java
+++ b/api-data-store-server-java/src/main/java/ch/ethz/sis/afsclient/client/AfsClient.java
@@ -1,6 +1,8 @@
 package ch.ethz.sis.afsclient.client;
 
 import java.io.ByteArrayInputStream;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
 import java.net.URI;
 import java.net.URLEncoder;
 import java.net.http.HttpClient;
@@ -8,10 +10,7 @@ import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
-import java.util.AbstractMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Stream;
 
 import ch.ethz.sis.afsapi.api.PublicAPI;
@@ -39,79 +38,109 @@ public final class AfsClient implements PublicAPI
 
     private final JsonObjectMapper jsonObjectMapper;
 
-    public AfsClient(final URI serverUri) {
+    public AfsClient(final URI serverUri)
+    {
         this(serverUri, DEFAULT_PACKAGE_SIZE_IN_BYTES, DEFAULT_TIMEOUT_IN_MILLIS);
     }
 
-    public AfsClient(final URI serverUri, final int maxReadSizeInBytes, final int timeout) {
+    public AfsClient(final URI serverUri, final int maxReadSizeInBytes, final int timeout)
+    {
         this.maxReadSizeInBytes = maxReadSizeInBytes;
         this.timeout = timeout;
         this.serverUri = serverUri;
         this.jsonObjectMapper = new JacksonObjectMapper();
     }
 
-    public URI getServerUri() {
+    public URI getServerUri()
+    {
         return serverUri;
     }
 
-    public int getMaxReadSizeInBytes() {
+    public int getMaxReadSizeInBytes()
+    {
         return maxReadSizeInBytes;
     }
 
-    public String getSessionToken() {
+    public String getSessionToken()
+    {
         return sessionToken;
     }
 
-    public void setSessionToken(final String sessionToken) {
+    public void setSessionToken(final String sessionToken)
+    {
         this.sessionToken = sessionToken;
     }
 
-    private static String urlEncode(final String s) {
+    private static String urlEncode(final String s)
+    {
         return URLEncoder.encode(s, StandardCharsets.UTF_8);
     }
 
     @Override
-    public @NonNull String login(@NonNull final String userId, @NonNull final String password) throws Exception {
-        return request("POST", "login", Map.of("userId", "admin", "password", "changeit"));
+    public @NonNull String login(@NonNull final String userId, @NonNull final String password)
+            throws Exception
+    {
+        String result = request("POST", "login", Map.of(),
+                (userId + ":" + password).getBytes());
+        setSessionToken(result);
+        return result;
     }
 
     @Override
-    public @NonNull Boolean isSessionValid() throws Exception {
-        return null;
+    public @NonNull Boolean isSessionValid() throws Exception
+    {
+        if (getSessionToken() == null)
+        {
+            throw new IllegalStateException("No session information detected!");
+        }
+        return request("GET", "isSessionValid", Map.of("sessionToken", getSessionToken()));
     }
 
     @Override
-    public @NonNull Boolean logout() throws Exception {
-        return null;
+    public @NonNull Boolean logout() throws Exception
+    {
+        if (getSessionToken() == null)
+        {
+            throw new IllegalStateException("No session information detected!");
+        }
+//      Boolean result = request("POST", "logout", Map.of(), getSessionToken().getBytes());
+        Boolean result = request("POST", "logout", Map.of("sessionToken", getSessionToken()));
+        setSessionToken(null);
+        return result;
     }
 
     @Override
     public @NonNull List<File> list(@NonNull final String owner, @NonNull final String source,
-            @NonNull final Boolean recursively) throws Exception {
+            @NonNull final Boolean recursively) throws Exception
+    {
         return null;
     }
 
     @Override
-    public byte @NonNull [] read(@NonNull final String owner, @NonNull final String source,
-            @NonNull final Long offset, @NonNull final Integer limit) throws Exception {
+    public @NonNull byte[] read(@NonNull final String owner, @NonNull final String source,
+            @NonNull final Long offset, @NonNull final Integer limit) throws Exception
+    {
         return new byte[0];
     }
 
     @Override
     public @NonNull Boolean write(@NonNull final String owner, @NonNull final String source,
             @NonNull final Long offset, final byte @NonNull [] data,
-            final byte @NonNull [] md5Hash) throws Exception {
+            final byte @NonNull [] md5Hash) throws Exception
+    {
         return null;
     }
 
     @Override
     public @NonNull Boolean delete(@NonNull final String owner, @NonNull final String source)
-            throws Exception {
+            throws Exception
+    {
         return null;
     }
 
     @Override
-    public @NonNull Boolean copy(@NonNull final String sourceOwner, @NonNull final String source, @NonNull final String targetOwner,
+    public @NonNull Boolean copy(@NonNull final String sourceOwner, @NonNull final String source,
+            @NonNull final String targetOwner,
             @NonNull final String target)
             throws Exception
     {
@@ -119,7 +148,8 @@ public final class AfsClient implements PublicAPI
     }
 
     @Override
-    public @NonNull Boolean move(@NonNull final String sourceOwner, @NonNull final String source, @NonNull final String targetOwner,
+    public @NonNull Boolean move(@NonNull final String sourceOwner, @NonNull final String source,
+            @NonNull final String targetOwner,
             @NonNull final String target)
             throws Exception
     {
@@ -127,53 +157,65 @@ public final class AfsClient implements PublicAPI
     }
 
     @Override
-    public void begin(final UUID transactionId) throws Exception {
+    public void begin(final UUID transactionId) throws Exception
+    {
 
     }
 
     @Override
-    public Boolean prepare() throws Exception {
+    public Boolean prepare() throws Exception
+    {
         return null;
     }
 
     @Override
-    public void commit() throws Exception {
+    public void commit() throws Exception
+    {
 
     }
 
     @Override
-    public void rollback() throws Exception {
+    public void rollback() throws Exception
+    {
 
     }
 
     @Override
-    public List<UUID> recover() throws Exception {
+    public List<UUID> recover() throws Exception
+    {
         return null;
     }
 
     private <T> T request(@NonNull final String httpMethod, @NonNull final String apiMethod,
-            @NonNull final Map<String, String> parameters) throws Exception {
+            @NonNull final Map<String, String> parameters) throws Exception
+    {
         return request(httpMethod, apiMethod, parameters, new byte[0]);
     }
 
     @SuppressWarnings({ "OptionalGetWithoutIsPresent", "unchecked" })
     private <T> T request(@NonNull final String httpMethod, @NonNull final String apiMethod,
-            @NonNull final Map<String, String> parameters, final byte @NonNull [] body) throws Exception {
-        HttpClient client = HttpClient.newBuilder()
+            @NonNull final Map<String, String> parameters, final byte @NonNull [] body)
+            throws Exception
+    {
+        HttpClient.Builder clientBuilder = HttpClient.newBuilder()
                 .version(HttpClient.Version.HTTP_1_1)
                 .followRedirects(HttpClient.Redirect.NORMAL)
-                .connectTimeout(Duration.ofMillis(timeout))
-                .build();
+                .connectTimeout(Duration.ofMillis(timeout));
+        Map<String, String> params = parameters;
+
+        HttpClient client = clientBuilder.build();
 
-        final String query = Stream.concat(Stream.of(new AbstractMap.SimpleImmutableEntry<>("method", apiMethod)),
-                        parameters.entrySet().stream())
+        final String query = Stream.concat(
+                        Stream.of(new AbstractMap.SimpleImmutableEntry<>("method", apiMethod)),
+                        params.entrySet().stream())
                 .map(entry -> urlEncode(entry.getKey()) + "=" + urlEncode(entry.getValue()))
                 .reduce((s1, s2) -> s1 + "&" + s2).get();
 
-        final URI uri = new URI(serverUri.getScheme(), null, serverUri.getHost(), serverUri.getPort(),
-                serverUri.getPath(), query, null);
+        final URI uri =
+                new URI(serverUri.getScheme(), null, serverUri.getHost(), serverUri.getPort(),
+                        serverUri.getPath(), query, null);
 
-        final HttpRequest.Builder builder = HttpRequest.newBuilder()
+        HttpRequest.Builder builder = HttpRequest.newBuilder()
                 .uri(uri)
                 .version(HttpClient.Version.HTTP_1_1)
                 .timeout(Duration.ofMillis(timeout))
@@ -181,23 +223,31 @@ public final class AfsClient implements PublicAPI
 
         final HttpRequest request = builder.build();
 
-        final HttpResponse<byte[]> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
+        final HttpResponse<byte[]> httpResponse =
+                client.send(request, HttpResponse.BodyHandlers.ofByteArray());
 
         final int statusCode = httpResponse.statusCode();
-        if (statusCode >= 200 && statusCode < 300) {
-            final ApiResponse response = jsonObjectMapper.readValue(new ByteArrayInputStream(httpResponse.body()),
-                    ApiResponse.class);
-
-            if (response.getError() != null) {
+        if (statusCode >= 200 && statusCode < 300)
+        {
+            final ApiResponse response =
+                    jsonObjectMapper.readValue(new ByteArrayInputStream(httpResponse.body()),
+                            ApiResponse.class);
+
+            if (response.getError() != null)
+            {
                 throw ClientExceptions.API_ERROR.getInstance(response.getError());
-            } else {
+            } else
+            {
                 return (T) response.getResult();
             }
-        } else if (statusCode >= 400 && statusCode < 500) {
+        } else if (statusCode >= 400 && statusCode < 500)
+        {
             throw ClientExceptions.CLIENT_ERROR.getInstance(statusCode);
-        } else if (statusCode >= 500 && statusCode < 600) {
+        } else if (statusCode >= 500 && statusCode < 600)
+        {
             throw ClientExceptions.SERVER_ERROR.getInstance(statusCode);
-        } else {
+        } else
+        {
             throw ClientExceptions.OTHER_ERROR.getInstance(statusCode);
         }
     }
diff --git a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java
index b4ada3b7708..a9ec352489f 100644
--- a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java
+++ b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/AfsClientTest.java
@@ -1,81 +1,117 @@
 package ch.ethz.sis.afsclient.client;
 
 import static org.junit.Assert.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.CoreMatchers.containsString;
 
 import java.net.URI;
 
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.*;
 
 public class AfsClientTest
 {
 
     private static DummyHttpServer httpServer;
 
-    private static AfsClient afsClient;
+    private AfsClient afsClient;
+    private static final int HTTP_SERVER_PORT = 8085;
+    private static final String HTTP_SERVER_PATH = "/fileserver";
 
-    private static int httpServerPort;
-
-    private static String httpServerPath;
-
-    @BeforeClass
-    public static void classSetUp() throws Exception
-    {
-        httpServerPort = 8085;
-        httpServerPath = "/fileserver";
-        httpServer = new DummyHttpServer(httpServerPort, httpServerPath);
+    @Before
+    public void setUp() throws Exception {
+        httpServer = new DummyHttpServer(HTTP_SERVER_PORT, HTTP_SERVER_PATH);
         httpServer.start();
         afsClient = new AfsClient(
-                new URI("http", null, "localhost", httpServerPort,
-                        httpServerPath, null, null));
+                new URI("http", null, "localhost", HTTP_SERVER_PORT,
+                        HTTP_SERVER_PATH, null, null));
     }
 
-    @AfterClass
-    public static void classTearDown() throws Exception
-    {
+    @After
+    public void tearDown() {
         httpServer.stop();
     }
 
     @Test
-    public void testLogin() throws Exception
+    public void login_methodIsPost() throws Exception
     {
         final String token = afsClient.login("test", "test");
         assertNotNull(token);
+        assertEquals(token, afsClient.getSessionToken());
+        assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
+    }
+
+    @Test
+    public void isSessionValid_withoutLogin_throwsException() throws Exception
+    {
+        try
+        {
+           afsClient.isSessionValid();
+            fail();
+        } catch (IllegalStateException e) {
+            assertThat(e.getMessage(), containsString("No session information detected!"));
+        }
     }
 
     @Test
-    public void testIsSessionValid()
+    public void isSessionValid_afterLogin_methodIsGet() throws Exception
     {
+        afsClient.login("test", "test");
+        httpServer.setNextResponse("{\"result\": true}");
+
+        Boolean result = afsClient.isSessionValid();
+
+        assertTrue(result);
+        assertEquals("GET", httpServer.getHttpExchange().getRequestMethod());
+    }
+
+    @Test
+    public void logout_withoutLogin_throwsException() throws Exception
+    {
+        try
+        {
+            afsClient.logout();
+            fail();
+        } catch (IllegalStateException e) {
+            assertThat(e.getMessage(), containsString("No session information detected!"));
+        }
     }
 
     @Test
-    public void testLogout()
+    public void logout_sessionTokenIsCleared() throws Exception
     {
+        afsClient.login("test", "test");
+        assertNotNull(afsClient.getSessionToken());
+
+        httpServer.setNextResponse("{\"result\": true}");
+
+        Boolean result = afsClient.logout();
+        assertTrue(result);
+        assertNull(afsClient.getSessionToken());
+        assertEquals("POST", httpServer.getHttpExchange().getRequestMethod());
     }
 
     @Test
-    public void testList()
+    public void testList() throws Exception
     {
     }
 
     @Test
-    public void testRead()
+    public void testRead()throws Exception
     {
     }
 
     @Test
-    public void testWrite()
+    public void testWrite()throws Exception
     {
     }
 
     @Test
-    public void testDelete()
+    public void testDelete()throws Exception
     {
     }
 
     @Test
-    public void testCopy()
+    public void testCopy()throws Exception
     {
     }
 
diff --git a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java
index 2450273d9a0..f4d9337fce7 100644
--- a/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java
+++ b/api-data-store-server-java/src/test/java/ch/ethz/sis/afsclient/client/DummyHttpServer.java
@@ -36,6 +36,7 @@ public final class DummyHttpServer
     private static final String DEFAULT_RESPONSE = "{\"result\": \"success\"}";
 
     private String nextResponse = DEFAULT_RESPONSE;
+    private HttpExchange httpExchange;
 
     public DummyHttpServer(int httpServerPort, String httpServerPath) throws IOException
     {
@@ -50,6 +51,7 @@ public final class DummyHttpServer
                 exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.length);
                 exchange.getResponseBody().write(response);
                 exchange.close();
+                httpExchange = exchange;
             }
         });
     }
@@ -69,4 +71,8 @@ public final class DummyHttpServer
         this.nextResponse = response;
     }
 
+    public HttpExchange getHttpExchange() {
+        return httpExchange;
+    }
+
 }
diff --git a/server-data-store/build.gradle b/server-data-store/build.gradle
index 35e6698c5b4..b468482a876 100644
--- a/server-data-store/build.gradle
+++ b/server-data-store/build.gradle
@@ -26,8 +26,8 @@ dependencies {
             'log4j:log4j-api:2.10.0',
             'log4j:log4j-core:2.10.0',
             'openbis:openbis-v3-api-batteries-included:20.10.5';
-    testImplementation 'junit:junit:4.10'
-    testRuntimeOnly 'hamcrest:hamcrest-core:1.3'
+    testImplementation 'junit:junit:4.10',
+            'hamcrest:hamcrest-core:1.3'
 }
 
 task AFSServerDevelopmentEnvironmentStart(type: JavaExec) {
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandler.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandler.java
index b9efb2ba185..50099d69e68 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandler.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpHandler.java
@@ -26,75 +26,96 @@ import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelInboundHandlerAdapter;
 import io.netty.handler.codec.http.*;
 
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 import java.util.Set;
 
 import static io.netty.handler.codec.http.HttpMethod.*;
 
-public class NettyHttpHandler extends ChannelInboundHandlerAdapter {
+public class NettyHttpHandler extends ChannelInboundHandlerAdapter
+{
 
     private static final Logger logger = LogManager.getLogger(NettyHttpServer.class);
+
     private static final byte[] NOT_FOUND = "404 NOT FOUND".getBytes();
+
     private static final ByteBuf NOT_FOUND_BUFFER = Unpooled.wrappedBuffer(NOT_FOUND);
+
     private static final Set<HttpMethod> allowedMethods = Set.of(GET, POST, PUT, DELETE);
 
     private final String uri;
+
     private final HttpServerHandler httpServerHandler;
 
-    public NettyHttpHandler(String uri, HttpServerHandler httpServerHandler) {
+    public NettyHttpHandler(String uri, HttpServerHandler httpServerHandler)
+    {
         this.uri = uri;
         this.httpServerHandler = httpServerHandler;
     }
 
     @Override
-    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
-        if (msg instanceof FullHttpRequest) {
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
+    {
+        if (msg instanceof FullHttpRequest)
+        {
             final FullHttpRequest request = (FullHttpRequest) msg;
             QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri(), true);
             if (queryStringDecoder.path().equals(uri) &&
-                    allowedMethods.contains(request.method())) {
+                    allowedMethods.contains(request.method()))
+            {
                 FullHttpResponse response = null;
                 ByteBuf content = request.content();
-                try {
-                    byte[] contentAsArray = (content.hasArray())?content.array():null;
-                    HttpResponse apiResponse = httpServerHandler.process(request.method(), queryStringDecoder.parameters(), contentAsArray);
-                    HttpResponseStatus status = (!apiResponse.isError())?HttpResponseStatus.OK:HttpResponseStatus.BAD_REQUEST;
+                try
+                {
+                    byte[] array = new byte[content.readableBytes()];
+                    content.readBytes(array);
+                    HttpResponse apiResponse = httpServerHandler.process(request.method(),
+                            queryStringDecoder.parameters(), array);
+                    HttpResponseStatus status = (!apiResponse.isError()) ?
+                            HttpResponseStatus.OK :
+                            HttpResponseStatus.BAD_REQUEST;
                     response = getHttpResponse(
                             status,
                             apiResponse.getContentType(),
                             Unpooled.wrappedBuffer(apiResponse.getBody()),
                             apiResponse.getBody().length);
                     ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
-                } finally {
+                } finally
+                {
                     content.release();
                 }
-            } else {
+            } else
+            {
                 FullHttpResponse response = getHttpResponse(
-                            HttpResponseStatus.NOT_FOUND,
-                            "text/plain",
-                            NOT_FOUND_BUFFER,
-                            NOT_FOUND.length);
+                        HttpResponseStatus.NOT_FOUND,
+                        "text/plain",
+                        NOT_FOUND_BUFFER,
+                        NOT_FOUND.length);
                 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
             }
-        } else {
+        } else
+        {
             super.channelRead(ctx, msg);
         }
     }
 
     @Override
-    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception
+    {
         ctx.flush();
     }
 
     @Override
-    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
+    {
         logger.catching(cause);
         byte[] causeBytes = cause.getMessage().getBytes();
         FullHttpResponse response = getHttpResponse(
-                    HttpResponseStatus.INTERNAL_SERVER_ERROR,
-                    "text/plain",
-                    Unpooled.wrappedBuffer(causeBytes),
-                    causeBytes.length
-                    );
+                HttpResponseStatus.INTERNAL_SERVER_ERROR,
+                "text/plain",
+                Unpooled.wrappedBuffer(causeBytes),
+                causeBytes.length
+        );
         ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
     }
 
@@ -102,7 +123,8 @@ public class NettyHttpHandler extends ChannelInboundHandlerAdapter {
             HttpResponseStatus status,
             String contentType,
             ByteBuf content,
-            int contentLength) {
+            int contentLength)
+    {
         FullHttpResponse response = new DefaultFullHttpResponse(
                 HttpVersion.HTTP_1_1,
                 status,
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpServer.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpServer.java
index b96132285bf..dbfa4dc78b0 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpServer.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/http/impl/NettyHttpServer.java
@@ -31,77 +31,101 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
 import io.netty.handler.codec.http.HttpServerCodec;
 import io.netty.util.concurrent.Future;
 
-public class NettyHttpServer implements HttpServer {
+public class NettyHttpServer implements HttpServer
+{
 
     private static final Logger logger = LogManager.getLogger(NettyHttpServer.class);
 
     private final EventLoopGroup masterGroup;
+
     private final EventLoopGroup slaveGroup;
 
     private ChannelFuture channel;
 
-    public NettyHttpServer() {
+    public NettyHttpServer()
+    {
         masterGroup = new NioEventLoopGroup();
         slaveGroup = new NioEventLoopGroup();
     }
 
-    public void start(int port, int maxContentLength, String uri, HttpServerHandler httpServerHandler) {
+    public void start(int port, int maxContentLength, String uri,
+            HttpServerHandler httpServerHandler)
+    {
         Integer maxQueueLengthForIncomingConnections = 128;
 
-        Runtime.getRuntime().addShutdownHook(new Thread() {
+        Runtime.getRuntime().addShutdownHook(new Thread()
+        {
             @Override
-            public void run() {
+            public void run()
+            {
                 shutdown(true);
             }
         });
 
-        try {
+        try
+        {
             final ServerBootstrap bootstrap = new ServerBootstrap()
                     .group(masterGroup, slaveGroup)
                     .channel(NioServerSocketChannel.class)
-                    .childHandler(new ChannelInitializer<SocketChannel>() {
+                    .childHandler(new ChannelInitializer<SocketChannel>()
+                    {
                         @Override
-                        public void initChannel(final SocketChannel ch) throws Exception {
+                        public void initChannel(final SocketChannel ch) throws Exception
+                        {
                             ch.pipeline().addLast("codec", new HttpServerCodec());
-                            ch.pipeline().addLast("aggregator", new HttpObjectAggregator(maxContentLength));
-                            ch.pipeline().addLast("request", new NettyHttpHandler(uri, httpServerHandler));
+                            ch.pipeline().addLast("aggregator",
+                                    new HttpObjectAggregator(maxContentLength));
+                            ch.pipeline().addLast("request",
+                                    new NettyHttpHandler(uri, httpServerHandler));
                         }
                     })
                     .option(ChannelOption.SO_BACKLOG, maxQueueLengthForIncomingConnections)
                     .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                     .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE);
             channel = bootstrap.bind(port).sync();
-        } catch (final Exception ex) {
+        } catch (final Exception ex)
+        {
             logger.catching(ex);
         }
     }
 
-    public void shutdown(boolean gracefully) {
-        try {
+    public void shutdown(boolean gracefully)
+    {
+        try
+        {
             channel.channel().close();
-        } catch (Exception ex) {
+        } catch (Exception ex)
+        {
             logger.catching(ex);
         }
 
-        try {
-            if (gracefully) {
+        try
+        {
+            if (gracefully)
+            {
                 Future slaveShutdown = slaveGroup.shutdownGracefully();
                 slaveShutdown.await();
-            } else {
+            } else
+            {
                 slaveGroup.shutdown();
             }
-        } catch (Exception ex) {
+        } catch (Exception ex)
+        {
             logger.catching(ex);
         }
 
-        try {
-            if (gracefully) {
+        try
+        {
+            if (gracefully)
+            {
                 Future masterShutdown = masterGroup.shutdownGracefully();
                 masterShutdown.await();
-            } else {
+            } else
+            {
                 masterGroup.shutdown();
             }
-        } catch (Exception ex) {
+        } catch (Exception ex)
+        {
             logger.catching(ex);
         }
     }
diff --git a/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java b/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
index ebaf263978d..3a8f9991c53 100644
--- a/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
+++ b/server-data-store/src/main/java/ch/ethz/sis/afsserver/server/impl/ApiServerAdapter.java
@@ -15,6 +15,8 @@
  */
 package ch.ethz.sis.afsserver.server.impl;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import ch.ethz.sis.afsserver.exception.HTTPExceptions;
 import ch.ethz.sis.afsserver.http.*;
 import ch.ethz.sis.afsserver.server.*;
@@ -31,50 +33,56 @@ import java.util.*;
 /*
  * This class is supposed to be called by a TCP or HTTP transport class
  */
-public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler {
+public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler
+{
 
     private static final Logger logger = LogManager.getLogger(ApiServerAdapter.class);
 
     private final APIServer<CONNECTION, Request, Response, API> server;
+
     private final JsonObjectMapper jsonObjectMapper;
-    private final ApiResponseBuilder apiResponseBuilder;
 
+    private final ApiResponseBuilder apiResponseBuilder;
 
     public ApiServerAdapter(
             APIServer<CONNECTION, Request, Response, API> server,
-            JsonObjectMapper jsonObjectMapper) {
+            JsonObjectMapper jsonObjectMapper)
+    {
         this.server = server;
         this.jsonObjectMapper = jsonObjectMapper;
         this.apiResponseBuilder = new ApiResponseBuilder();
     }
 
-    public static HttpMethod getHttpMethod(String apiMethod) {
-        HttpMethod httpMethod = null;
-        switch (apiMethod){
-            case "delete":
-                httpMethod = HttpMethod.DELETE;
-                break;
-            case "write":
-                httpMethod = HttpMethod.PUT;
-                break;
+    public static HttpMethod getHttpMethod(String apiMethod)
+    {
+        switch (apiMethod)
+        {
             case "list":
             case "read":
             case "isSessionValid":
-                httpMethod = HttpMethod.GET;
-                break;
-            default:
-                httpMethod = HttpMethod.POST;
+                return HttpMethod.GET; // all parameters from GET methods come on the query string
+            case "write":
+            case "move":
+            case "login":
+            case "logout":
+                return HttpMethod.POST; // all parameters from POST methods come on the body
+            case "delete":
+                return HttpMethod.DELETE; // all parameters from DELETE methods come on the body
         }
-        return httpMethod;
+        throw new UnsupportedOperationException("This line SHOULD NOT be unreachable!");
     }
 
-    public boolean isValidMethod(HttpMethod givenMethod, String apiMethod) {
+    public boolean isValidMethod(HttpMethod givenMethod, String apiMethod)
+    {
         HttpMethod correctMethod = getHttpMethod(apiMethod);
         return correctMethod == givenMethod;
     }
 
-    public HttpResponse process(HttpMethod httpMethod, Map<String, List<String>> uriParameters, byte[] requestBody) {
-        try {
+    public HttpResponse process(HttpMethod httpMethod, Map<String, List<String>> uriParameters,
+            byte[] requestBody)
+    {
+        try
+        {
             logger.traceAccess(null);
             PerformanceAuditor performanceAuditor = new PerformanceAuditor();
 
@@ -83,22 +91,31 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler {
             String interactiveSessionKey = null;
             String transactionManagerKey = null;
             Map<String, Object> methodParameters = new HashMap<>();
-            for (Map.Entry<String, List<String>> entry:uriParameters.entrySet()) {
+            for (Map.Entry<String, List<String>> entry : uriParameters.entrySet())
+            {
                 String value = null;
-                if (entry.getValue() != null) {
-                    if (entry.getValue().size() == 1) {
+                if (entry.getValue() != null)
+                {
+                    if (entry.getValue().size() == 1)
+                    {
                         value = entry.getValue().get(0);
-                    } else if (entry.getValue().size() > 1) {
-                        return getHTTPResponse(new ApiResponse("1", null, HTTPExceptions.INVALID_PARAMETERS.getCause()));
+                    } else if (entry.getValue().size() > 1)
+                    {
+                        return getHTTPResponse(new ApiResponse("1", null,
+                                HTTPExceptions.INVALID_PARAMETERS.getCause()));
                     }
                 }
 
-                try {
-                    switch (entry.getKey()) {
+                try
+                {
+                    switch (entry.getKey())
+                    {
                         case "method":
                             method = value;
-                            if (!isValidMethod(httpMethod, method)) {
-                                return getHTTPResponse(new ApiResponse("1", null, HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
+                            if (!isValidMethod(httpMethod, method))
+                            {
+                                return getHTTPResponse(new ApiResponse("1", null,
+                                        HTTPExceptions.INVALID_HTTP_METHOD.getCause()));
                             }
                             break;
                         case "sessionToken":
@@ -129,54 +146,80 @@ public class ApiServerAdapter<CONNECTION, API> implements HttpServerHandler {
                             methodParameters.put(entry.getKey(), value);
                             break;
                     }
-                } catch (Exception e) {
+                } catch (Exception e)
+                {
                     logger.catching(e);
-                    return getHTTPResponse(new ApiResponse("1", null, HTTPExceptions.INVALID_PARAMETERS.getCause(e.getClass().getSimpleName(), e.getMessage())));
+                    return getHTTPResponse(new ApiResponse("1", null,
+                            HTTPExceptions.INVALID_PARAMETERS.getCause(e.getClass().getSimpleName(),
+                                    e.getMessage())));
                 }
             }
 
-            if (method.equals("write")) {
-                methodParameters.put("data", requestBody);
+            switch (method) {
+                case "write":
+                    methodParameters.put("data", requestBody);
+                    break;
+                case "login":
+                    // userId : password
+                    String[] credentials = new String(requestBody, UTF_8).split(":");
+                    methodParameters.put("userId", credentials[0]);
+                    methodParameters.put("password", credentials[1]);
+                    break;
             }
 
-            ApiRequest apiRequest = new ApiRequest("1", method, methodParameters, sessionToken, interactiveSessionKey, transactionManagerKey);
-            Response response = server.processOperation(apiRequest, apiResponseBuilder, performanceAuditor);
+
+            ApiRequest apiRequest = new ApiRequest("1", method, methodParameters, sessionToken,
+                    interactiveSessionKey, transactionManagerKey);
+            Response response =
+                    server.processOperation(apiRequest, apiResponseBuilder, performanceAuditor);
             HttpResponse httpResponse = getHTTPResponse(response);
             performanceAuditor.audit(Event.WriteResponse);
             logger.traceExit(performanceAuditor);
             logger.traceExit(httpResponse);
             return httpResponse;
-        } catch (APIServerException e) {
+        } catch (APIServerException e)
+        {
             logger.catching(e);
-            switch (e.getType()) {
+            switch (e.getType())
+            {
                 case MethodNotFound:
                 case IncorrectParameters:
                 case InternalError:
-                    try {
+                    try
+                    {
                         return getHTTPResponse(new ApiResponse("1", null, e.getData()));
-                    } catch (Exception ex) {
+                    } catch (Exception ex)
+                    {
                         logger.catching(ex);
                     }
             }
-        } catch (Exception e) {
+        } catch (Exception e)
+        {
             logger.catching(e);
-            try {
-                return getHTTPResponse(new ApiResponse("1", null, HTTPExceptions.UNKNOWN.getCause(e.getClass().getSimpleName(), e.getMessage())));
-            } catch (Exception ex) {
+            try
+            {
+                return getHTTPResponse(new ApiResponse("1", null,
+                        HTTPExceptions.UNKNOWN.getCause(e.getClass().getSimpleName(),
+                                e.getMessage())));
+            } catch (Exception ex)
+            {
                 logger.catching(ex);
             }
         }
         return null; // This should never happen, it would mean an error writing the Unknown error happened.
     }
 
-    private HttpResponse getHTTPResponse(Response response) throws Exception {
+    private HttpResponse getHTTPResponse(Response response) throws Exception
+    {
         boolean error = response.getError() != null;
         String contentType = null;
         byte[] body = null;
-        if (response.getResult() instanceof byte[]) {
+        if (response.getResult() instanceof byte[])
+        {
             contentType = HttpResponse.CONTENT_TYPE_BINARY_DATA;
             body = (byte[]) response.getResult();
-        } else {
+        } else
+        {
             contentType = HttpResponse.CONTENT_TYPE_JSON;
             body = jsonObjectMapper.writeValue(response);
         }
diff --git a/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java b/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java
index d443be04577..e9065b60241 100644
--- a/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java
+++ b/server-data-store/src/test/java/ch/ethz/sis/afsserver/ApiClientTest.java
@@ -17,14 +17,14 @@
 
 package ch.ethz.sis.afsserver;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.*;
 
 import java.net.URI;
 import java.util.List;
 
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.*;
 
 import ch.ethz.sis.afs.manager.TransactionConnection;
 import ch.ethz.sis.afsclient.client.AfsClient;
@@ -39,6 +39,10 @@ public final class ApiClientTest
 
     private static AfsClient afsClient;
 
+    private static int httpServerPort;
+
+    private static String httpServerPath;
+
     @BeforeClass
     public static void classSetUp() throws Exception
     {
@@ -47,16 +51,25 @@ public final class ApiClientTest
                         "src/test/resources/test-server-config.properties");
         final DummyServerObserver dummyServerObserver = new DummyServerObserver();
         afsServer = new Server<>(configuration, dummyServerObserver, dummyServerObserver);
-
-        final int httpServerPort =
+        httpServerPort =
                 configuration.getIntegerProperty(AtomicFileSystemServerParameter.httpServerPort);
-        final String httpServerPath =
+        httpServerPath =
                 configuration.getStringProperty(AtomicFileSystemServerParameter.httpServerUri);
+    }
+
+    @Before
+    public void setUp() throws Exception
+    {
         afsClient = new AfsClient(
                 new URI("http", null, "localhost", httpServerPort,
                         httpServerPath, null, null));
     }
 
+    private String login() throws Exception
+    {
+        return afsClient.login("test", "test");
+    }
+
     @AfterClass
     public static void classTearDown() throws Exception
     {
@@ -64,10 +77,55 @@ public final class ApiClientTest
     }
 
     @Test
-    public void testLogin() throws Exception
+    public void login_sessionTokenIsNotNull() throws Exception
     {
-        final String token = afsClient.login("test", "test");
+        final String token = login();
         assertNotNull(token);
     }
 
+    @Test
+    public void isSessionValid_throwsException() throws Exception
+    {
+        try
+        {
+            afsClient.isSessionValid();
+            fail();
+        } catch (IllegalStateException e)
+        {
+            assertThat(e.getMessage(), containsString("No session information detected!"));
+        }
+    }
+
+    @Test
+    public void isSessionValid_returnsTrue() throws Exception
+    {
+        login();
+
+        final Boolean isValid = afsClient.isSessionValid();
+        assertTrue(isValid);
+    }
+
+    @Test
+    public void logout_withoutLogin_throwsException() throws Exception
+    {
+        try
+        {
+            afsClient.logout();
+            fail();
+        } catch (IllegalStateException e)
+        {
+            assertThat(e.getMessage(), containsString("No session information detected!"));
+        }
+    }
+
+    @Test
+    public void logout_withLogin_returnsTrue() throws Exception
+    {
+        login();
+
+        final Boolean result = afsClient.logout();
+
+        assertTrue(result);
+    }
+
 }
-- 
GitLab