From 5390ffb8ce79effb0925fb9e3ea827d917c315bd Mon Sep 17 00:00:00 2001
From: juanf <juanf@ethz.ch>
Date: Mon, 5 Dec 2022 17:24:17 +0100
Subject: [PATCH] SSDM-13154 : New DSS Server proxy layers, adding basic tests
 for the api server

---
 afs-server/build.gradle                       |  13 +-
 .../ethz/sis/afsserver/api/OperationsAPI.java |   8 +-
 .../ethz/sis/afsserver/server/APIServer.java  |   1 +
 .../sis/afsserver/server/ThrowableReason.java |  15 ---
 .../impl/DummyAuthenticationInfoProvider.java |  15 +--
 .../worker/proxy/AuthenticationProxy.java     |   3 +-
 .../ch/ethz/sis/afsserver/AbstractTest.java   |  27 ++++
 ...nt.java => ServerClientEnvironmentFS.java} |  37 +++++-
 .../java/ch/ethz/sis/afsserver/TestSuite.java |  14 +++
 .../core/AbstractPublicAPIWrapper.java        | 117 ++++++++++++++++++
 .../sis/afsserver/core/PublicApiTest.java     | 108 ++++++++++++++++
 .../sis/afsserver/impl/ApiServerTest.java     |  40 ++++++
 .../impl/PublicAPIAdapterWrapper.java         |  41 ++++++
 afs/build.gradle                              |   4 +
 14 files changed, 402 insertions(+), 41 deletions(-)
 delete mode 100644 afs-server/src/main/java/ch/ethz/sis/afsserver/server/ThrowableReason.java
 create mode 100644 afs-server/src/test/java/ch/ethz/sis/afsserver/AbstractTest.java
 rename afs-server/src/test/java/ch/ethz/sis/afsserver/{AFSServerEnvironment.java => ServerClientEnvironmentFS.java} (72%)
 create mode 100644 afs-server/src/test/java/ch/ethz/sis/afsserver/TestSuite.java
 create mode 100644 afs-server/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java
 create mode 100644 afs-server/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java
 create mode 100644 afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java
 create mode 100644 afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java

diff --git a/afs-server/build.gradle b/afs-server/build.gradle
index 33e767a77bb..781cca0b76a 100644
--- a/afs-server/build.gradle
+++ b/afs-server/build.gradle
@@ -1,6 +1,10 @@
 apply plugin: 'java'
 apply plugin: 'application'
 
+compileJava {
+    options.compilerArgs << '-parameters'
+}
+
 repositories {
     ivy {
         ivyPattern "https://sissource.ethz.ch/openbis/openbis-public/openbis-ivy/-/raw/main/[organisation]/[module]/[revision]/ivy.xml"
@@ -9,19 +13,14 @@ repositories {
 }
 
 dependencies {
-    testImplementation 'junit:junit:4.10'
-    testRuntimeOnly 'junit:junit:4.10'
-
     annotationProcessor 'lombok:lombok:1.18.22'
     implementation project(':afs'),
             'lombok:lombok:1.18.22',
             'io.netty:netty-all:4.1.68.Final',
             'log4j:log4j-api:2.10.0',
             'log4j:log4j-core:2.10.0';
-}
-
-test {
-    useJUnitPlatform()
+    testImplementation 'junit:junit:4.10'
+    testRuntimeOnly 'hamcrest:hamcrest-core:1.3'
 }
 
 task AFSServerDevelopmentEnvironmentStart(type: JavaExec) {
diff --git a/afs-server/src/main/java/ch/ethz/sis/afsserver/api/OperationsAPI.java b/afs-server/src/main/java/ch/ethz/sis/afsserver/api/OperationsAPI.java
index 80332607299..0cb868d38e1 100644
--- a/afs-server/src/main/java/ch/ethz/sis/afsserver/api/OperationsAPI.java
+++ b/afs-server/src/main/java/ch/ethz/sis/afsserver/api/OperationsAPI.java
@@ -25,19 +25,19 @@ public interface OperationsAPI
 {
 
     @NonNull
-    List<File> list(@NonNull String sourceOwner, @NonNull String source, @NonNull Boolean recursively)
+    List<File> list(@NonNull String owner, @NonNull String source, @NonNull Boolean recursively)
             throws Exception;
 
     @NonNull
-    byte[] read(@NonNull String sourceOwner, @NonNull String source, @NonNull Long offset,
+    byte[] read(@NonNull String owner, @NonNull String source, @NonNull Long offset,
             @NonNull Integer limit) throws Exception;
 
     @NonNull
-    Boolean write(@NonNull String sourceOwner, @NonNull String source, @NonNull Long offset,
+    Boolean write(@NonNull String owner, @NonNull String source, @NonNull Long offset,
             @NonNull byte[] data, @NonNull byte[] md5Hash) throws Exception;
 
     @NonNull
-    Boolean delete(@NonNull String sourceOwner, @NonNull String source) throws Exception;
+    Boolean delete(@NonNull String owner, @NonNull String source) throws Exception;
 
     @NonNull
     Boolean copy(@NonNull String sourceOwner, @NonNull String source, @NonNull String targetOwner,
diff --git a/afs-server/src/main/java/ch/ethz/sis/afsserver/server/APIServer.java b/afs-server/src/main/java/ch/ethz/sis/afsserver/server/APIServer.java
index b6c0a0ab618..2f095ade869 100644
--- a/afs-server/src/main/java/ch/ethz/sis/afsserver/server/APIServer.java
+++ b/afs-server/src/main/java/ch/ethz/sis/afsserver/server/APIServer.java
@@ -5,6 +5,7 @@ import ch.ethz.sis.afs.api.dto.ExceptionReason;
 import ch.ethz.sis.afsserver.exception.APIExceptions;
 import ch.ethz.sis.afsserver.server.observer.APIServerObserver;
 import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor;
+import ch.ethz.sis.shared.exception.ThrowableReason;
 import ch.ethz.sis.shared.log.LogManager;
 import ch.ethz.sis.shared.log.Logger;
 import ch.ethz.sis.shared.pool.Pool;
diff --git a/afs-server/src/main/java/ch/ethz/sis/afsserver/server/ThrowableReason.java b/afs-server/src/main/java/ch/ethz/sis/afsserver/server/ThrowableReason.java
deleted file mode 100644
index a64f560cc97..00000000000
--- a/afs-server/src/main/java/ch/ethz/sis/afsserver/server/ThrowableReason.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package ch.ethz.sis.afsserver.server;
-
-import java.io.Serializable;
-
-public class ThrowableReason extends Throwable {
-    private Serializable reason;
-
-    public ThrowableReason(Serializable reason) {
-        this.reason = reason;
-    }
-
-    public Serializable getReason() {
-        return reason;
-    }
-}
diff --git a/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/DummyAuthenticationInfoProvider.java b/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/DummyAuthenticationInfoProvider.java
index bed1bd906bc..95a34af6fd3 100644
--- a/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/DummyAuthenticationInfoProvider.java
+++ b/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/providers/impl/DummyAuthenticationInfoProvider.java
@@ -3,16 +3,9 @@ package ch.ethz.sis.afsserver.worker.providers.impl;
 import ch.ethz.sis.afsserver.worker.providers.AuthenticationInfoProvider;
 import ch.ethz.sis.shared.startup.Configuration;
 
-import java.util.HashSet;
-import java.util.Set;
 import java.util.UUID;
 
 public class DummyAuthenticationInfoProvider implements AuthenticationInfoProvider {
-    private final Set<String> dummySessions;
-
-    private DummyAuthenticationInfoProvider() {
-        dummySessions = new HashSet<>();
-    }
 
     @Override
     public void init(Configuration initParameter) throws Exception {
@@ -21,18 +14,16 @@ public class DummyAuthenticationInfoProvider implements AuthenticationInfoProvid
 
     @Override
     public String login(String userId, String password) {
-        String sessionToken = UUID.randomUUID().toString();
-        dummySessions.add(sessionToken);
-        return sessionToken;
+        return UUID.randomUUID().toString();
     }
 
     @Override
     public Boolean isSessionValid(String sessionToken) {
-        return dummySessions.contains(sessionToken);
+        return sessionToken != null;
     }
 
     @Override
     public Boolean logout(String sessionToken) {
-        return dummySessions.remove(sessionToken);
+        return sessionToken != null;
     }
 }
diff --git a/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/proxy/AuthenticationProxy.java b/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/proxy/AuthenticationProxy.java
index 04b55474a33..c885b1cb95d 100644
--- a/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/proxy/AuthenticationProxy.java
+++ b/afs-server/src/main/java/ch/ethz/sis/afsserver/worker/proxy/AuthenticationProxy.java
@@ -129,7 +129,8 @@ public class AuthenticationProxy extends AbstractProxy {
 
     private void validateSessionAvailable() throws Exception {
         if (workerContext.getSessionExists() == null) {
-            boolean doSessionExists = authenticationInfoProvider.isSessionValid(workerContext.getSessionToken());
+            String sessionToken = workerContext.getSessionToken();
+            boolean doSessionExists = authenticationInfoProvider.isSessionValid(sessionToken);
             workerContext.setSessionExists(doSessionExists);
         }
         if (!workerContext.getSessionExists()) {
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/AbstractTest.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/AbstractTest.java
new file mode 100644
index 00000000000..8a34ec09de1
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/AbstractTest.java
@@ -0,0 +1,27 @@
+package ch.ethz.sis.afsserver;
+
+import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
+import ch.ethz.sis.shared.log.LogFactory;
+import ch.ethz.sis.shared.log.LogFactoryFactory;
+import ch.ethz.sis.shared.log.LogManager;
+
+import java.io.File;
+
+
+public class AbstractTest {
+    static {
+        try {
+            init();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    static private void init() throws Exception {
+        System.out.println("Current Working Directory: " + (new File("")).getCanonicalPath());
+        // Initializing LogManager
+        LogFactoryFactory logFactoryFactory = new LogFactoryFactory();
+        LogFactory logFactory = logFactoryFactory.create(ServerClientEnvironmentFS.getInstance().getDefaultServerConfiguration().getStringProperty(AtomicFileSystemServerParameter.logFactoryClass));
+        LogManager.setLogFactory(logFactory);
+    }
+}
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/AFSServerEnvironment.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/ServerClientEnvironmentFS.java
similarity index 72%
rename from afs-server/src/test/java/ch/ethz/sis/afsserver/AFSServerEnvironment.java
rename to afs-server/src/test/java/ch/ethz/sis/afsserver/ServerClientEnvironmentFS.java
index 578e1b6714a..64be51b4930 100644
--- a/afs-server/src/test/java/ch/ethz/sis/afsserver/AFSServerEnvironment.java
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/ServerClientEnvironmentFS.java
@@ -18,6 +18,10 @@ package ch.ethz.sis.afsserver;
 
 import ch.ethz.sis.afsserver.api.PublicAPI;
 import ch.ethz.sis.afsserver.http.impl.NettyHttpServer;
+import ch.ethz.sis.afsserver.server.Server;
+import ch.ethz.sis.afsserver.server.observer.APIServerObserver;
+import ch.ethz.sis.afsserver.server.observer.ServerObserver;
+import ch.ethz.sis.afsserver.server.observer.impl.DummyServerObserver;
 import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
 import ch.ethz.sis.afsserver.worker.ConnectionFactory;
 import ch.ethz.sis.afsserver.worker.WorkerFactory;
@@ -30,8 +34,37 @@ import ch.ethz.sis.shared.startup.Configuration;
 import java.util.HashMap;
 import java.util.Map;
 
-public class AFSServerEnvironment {
-    public static Configuration getDefaultAFSConfig() {
+public class ServerClientEnvironmentFS {
+
+    private static ServerClientEnvironmentFS instance;
+
+    public static ServerClientEnvironmentFS getInstance() {
+        if (instance == null) {
+            synchronized (ServerClientEnvironmentFS.class) {
+                instance = new ServerClientEnvironmentFS();
+            }
+        }
+        return instance;
+    }
+
+    private ServerClientEnvironmentFS() {
+
+    }
+
+    public Server start() throws Exception {
+        DummyServerObserver observer = new DummyServerObserver();
+        return new Server(getDefaultServerConfiguration(), observer, observer);
+    }
+
+    public <E extends ServerObserver & APIServerObserver> Server start(Configuration configuration, E serverObserver) throws Exception {
+        return new Server(configuration, serverObserver, serverObserver);
+    }
+
+    public void stop(Server server, boolean gracefully) throws Exception {
+        server.shutdown(gracefully);
+    }
+
+    public static Configuration getDefaultServerConfiguration() {
         Map<Enum, String> configuration = new HashMap<>();
         configuration.put(AtomicFileSystemServerParameter.logFactoryClass,  Log4J2LogFactory.class.getName());
 //        configuration.put(AtomicFileSystemServerParameter.logConfigFile,  "objectfs-afs-config-log4j2.xml");
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/TestSuite.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/TestSuite.java
new file mode 100644
index 00000000000..88b21d6cffa
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/TestSuite.java
@@ -0,0 +1,14 @@
+package ch.ethz.sis.afsserver;
+
+import ch.ethz.sis.afsserver.impl.ApiServerTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+        ApiServerTest.class
+})
+
+public class TestSuite {
+
+}
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java
new file mode 100644
index 00000000000..34db1689db6
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/core/AbstractPublicAPIWrapper.java
@@ -0,0 +1,117 @@
+package ch.ethz.sis.afsserver.core;
+
+import ch.ethz.sis.afs.api.dto.File;
+import ch.ethz.sis.afsserver.api.PublicAPI;
+import lombok.NonNull;
+
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+public abstract class AbstractPublicAPIWrapper implements PublicAPI {
+
+    public abstract <E> E process(String method, Map<String, Object> methodParameters);
+
+    @Override
+    public List<File> list(@NonNull String owner, @NonNull String source, @NonNull Boolean recursively) throws Exception {
+        Map<String, Object> args = Map.of(
+                "owner", owner,
+                "source", source,
+                "recursively", recursively);
+        return process("list", args);
+    }
+
+    @Override
+    public byte[] read(@NonNull String owner, @NonNull String source, @NonNull Long offset, @NonNull Integer limit) throws Exception {
+        Map<String, Object> args = Map.of(
+                "owner", owner,
+                "source", source,
+                "offset", offset,
+                "limit", limit);
+        return process("read", args);
+    }
+
+    @Override
+    public Boolean write(@NonNull String owner, @NonNull String source, @NonNull Long offset, @NonNull byte[] data, @NonNull byte[] md5Hash) throws Exception {
+        Map<String, Object> args = Map.of(
+                "owner", owner,
+                "source", source,
+                "offset", offset,
+                "data", data,
+                "md5Hash", md5Hash);
+        return process("write", args);
+    }
+
+    @Override
+    public Boolean delete(@NonNull String owner, @NonNull String source) throws Exception {
+        Map<String, Object> args = Map.of(
+                "owner", owner,
+                "source", source);
+        return process("delete", args);
+    }
+
+    @Override
+    public Boolean copy(@NonNull String sourceOwner, @NonNull String source, @NonNull String targetOwner, @NonNull String target) throws Exception {
+        Map<String, Object> args = Map.of(
+                "sourceOwner", sourceOwner,
+                "source", source,
+                "targetOwner", targetOwner,
+                "target", target);
+        return process("copy", args);
+    }
+
+    @Override
+    public Boolean move(@NonNull String sourceOwner, @NonNull String source, @NonNull String targetOwner, @NonNull String target) throws Exception {
+        Map<String, Object> args = Map.of(
+                "sourceOwner", sourceOwner,
+                "source", source,
+                "targetOwner", targetOwner,
+                "target", target);
+        return process("move", args);
+    }
+
+    @Override
+    public void begin(UUID transactionId) throws Exception {
+        //TODO: Unused
+    }
+
+    @Override
+    public Boolean prepare() throws Exception {
+        //TODO: Unused
+        return true;
+    }
+
+    @Override
+    public void commit() throws Exception {
+        //TODO: Unused
+    }
+
+    @Override
+    public void rollback() throws Exception {
+        //TODO: Unused
+    }
+
+    @Override
+    public List<UUID> recover() throws Exception {
+        //TODO: Unused
+        return null;
+    }
+
+    @Override
+    public String login(String userId, String password) throws Exception {
+        //TODO: Unused
+        return null;
+    }
+
+    @Override
+    public Boolean isSessionValid() throws Exception {
+        //TODO: Unused
+        return null;
+    }
+
+    @Override
+    public Boolean logout() throws Exception {
+        //TODO: Unused
+        return null;
+    }
+}
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java
new file mode 100644
index 00000000000..22a1e46a142
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/core/PublicApiTest.java
@@ -0,0 +1,108 @@
+package ch.ethz.sis.afsserver.core;
+
+import ch.ethz.sis.afs.api.dto.File;
+import ch.ethz.sis.afsserver.AbstractTest;
+import ch.ethz.sis.afsserver.ServerClientEnvironmentFS;
+import ch.ethz.sis.afsserver.api.PublicAPI;
+import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
+import ch.ethz.sis.shared.exception.ThrowableReason;
+import ch.ethz.sis.shared.io.IOUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public abstract class PublicApiTest extends AbstractTest {
+
+    public abstract PublicAPI getPublicAPI() throws Exception;
+
+    public static final String ROOT = IOUtils.PATH_SEPARATOR_AS_STRING;
+//    public static final String DIR_A = "A";
+//    public static final String DIR_B = "B";
+    public static final String FILE_A = "A.txt";
+    public static final byte[] DATA = "ABCD".getBytes();
+    public static final String FILE_B = "B.txt";
+//    public static final String DIR_A_PATH = IOUtils.PATH_SEPARATOR + getPath(DIR_A);
+//    public static final String DIR_B_PATH = IOUtils.PATH_SEPARATOR + getPath(DIR_B);
+//    public static final String FILE_A_PATH = getPath(DIR_A_PATH, FILE_A);
+//    public static final String FILE_B_PATH = getPath(DIR_B_PATH, FILE_B);
+
+    public String owner = UUID.randomUUID().toString();
+
+    @Before
+    public void createTestData() throws IOException {
+        String storageRoot = ServerClientEnvironmentFS.getInstance()
+                .getDefaultServerConfiguration().getStringProperty(AtomicFileSystemServerParameter.storageRoot);
+        String testDataRoot = IOUtils.getPath(storageRoot, owner.toString());
+        IOUtils.createDirectories(testDataRoot);
+        String testDataFile = IOUtils.getPath(testDataRoot, FILE_A);
+        IOUtils.createFile(testDataFile);
+        IOUtils.write(testDataFile, 0, DATA);
+    }
+
+    @After
+    public void deleteTestData() throws IOException {
+        String storageRoot = ServerClientEnvironmentFS.getInstance()
+                .getDefaultServerConfiguration().getStringProperty(AtomicFileSystemServerParameter.storageRoot);
+        IOUtils.delete(storageRoot);
+        String writeAheadLogRoot = ServerClientEnvironmentFS.getInstance()
+                .getDefaultServerConfiguration().getStringProperty(AtomicFileSystemServerParameter.writeAheadLogRoot);
+        IOUtils.delete(writeAheadLogRoot);
+    }
+
+    @Test
+    public void list() throws Exception {
+        List<File> list = getPublicAPI().list(owner, ROOT, Boolean.TRUE);
+        assertEquals(1, list.size());
+        assertEquals(FILE_A, list.get(0).getName());
+    }
+
+    @Test
+    public void read() throws Exception {
+        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_A, 0L, DATA.length);
+        assertArrayEquals(DATA, bytes);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void read_big_failure() throws Exception {
+        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_A, 0L, Integer.MAX_VALUE);
+        assertArrayEquals(DATA, bytes);
+    }
+
+    @Test
+    public void write() throws Exception {
+        getPublicAPI().write(owner, "/" + FILE_B, 0L, DATA, IOUtils.getMD5(DATA));
+        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_B, 0L, DATA.length);
+        assertArrayEquals(DATA, bytes);
+    }
+
+    @Test
+    public void delete() throws Exception {
+        Boolean deleted = getPublicAPI().delete(owner, "/" + FILE_A);
+        assertTrue(deleted);
+        List<File> list = getPublicAPI().list(owner, ROOT, Boolean.TRUE);
+        assertEquals(0, list.size());
+    }
+
+    @Test
+    public void copy() throws Exception {
+        getPublicAPI().copy(owner, "/" + FILE_A, owner, "/" + FILE_B);
+        byte[] bytes = getPublicAPI().read(owner, "/" + FILE_B, 0L, DATA.length);
+        assertArrayEquals(DATA, bytes);
+    }
+
+    @Test
+    public void move() throws Exception {
+        getPublicAPI().move(owner, "/" + FILE_A, owner, "/" + FILE_B);
+        List<File> list = getPublicAPI().list(owner, ROOT, Boolean.TRUE);
+        assertEquals(1, list.size());
+        assertEquals(FILE_B, list.get(0).getName());
+    }
+}
\ No newline at end of file
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java
new file mode 100644
index 00000000000..e88563710e7
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/ApiServerTest.java
@@ -0,0 +1,40 @@
+package ch.ethz.sis.afsserver.impl;
+
+import ch.ethz.sis.afs.manager.TransactionConnection;
+import ch.ethz.sis.afsserver.ServerClientEnvironmentFS;
+import ch.ethz.sis.afsserver.api.PublicAPI;
+import ch.ethz.sis.afsserver.core.PublicApiTest;
+import ch.ethz.sis.afsserver.server.APIServer;
+import ch.ethz.sis.afsserver.server.Worker;
+import ch.ethz.sis.afsserver.server.observer.impl.DummyServerObserver;
+import ch.ethz.sis.afsserver.startup.AtomicFileSystemServerParameter;
+import ch.ethz.sis.afsserver.worker.ConnectionFactory;
+import ch.ethz.sis.afsserver.worker.WorkerFactory;
+import ch.ethz.sis.shared.pool.Pool;
+import ch.ethz.sis.shared.startup.Configuration;
+
+public class ApiServerTest extends PublicApiTest {
+
+    @Override
+    public PublicAPI getPublicAPI() throws Exception {
+        Configuration configuration = ServerClientEnvironmentFS.getInstance().getDefaultServerConfiguration();
+
+        ConnectionFactory connectionFactory = new ConnectionFactory();
+        connectionFactory.init(configuration);
+
+        WorkerFactory workerFactory = new WorkerFactory();
+        int poolSize = configuration.getIntegerProperty(AtomicFileSystemServerParameter.poolSize);
+
+        Pool<Configuration, TransactionConnection> connectionsPool = new Pool<>(poolSize, configuration, connectionFactory);
+        Pool<Configuration, Worker> workersPool = new Pool<>(poolSize, configuration, workerFactory);
+
+        String interactiveSessionKey = configuration.getStringProperty(AtomicFileSystemServerParameter.apiServerInteractiveSessionKey);
+        String transactionManagerKey = configuration.getStringProperty(AtomicFileSystemServerParameter.apiServerTransactionManagerKey);
+        int apiServerWorkerTimeout = configuration.getIntegerProperty(AtomicFileSystemServerParameter.apiServerWorkerTimeout);
+        DummyServerObserver observer = new DummyServerObserver();
+        observer.init(configuration);
+        APIServer apiServer = new APIServer(connectionsPool, workersPool, PublicAPI.class, interactiveSessionKey, transactionManagerKey, apiServerWorkerTimeout, observer);
+        observer.init(apiServer, configuration);
+        return new PublicAPIAdapterWrapper(apiServer);
+    }
+}
diff --git a/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java
new file mode 100644
index 00000000000..42e5ba48c0a
--- /dev/null
+++ b/afs-server/src/test/java/ch/ethz/sis/afsserver/impl/PublicAPIAdapterWrapper.java
@@ -0,0 +1,41 @@
+package ch.ethz.sis.afsserver.impl;
+
+
+import ch.ethz.sis.afsserver.core.AbstractPublicAPIWrapper;
+import ch.ethz.sis.afsserver.server.APIServer;
+import ch.ethz.sis.afsserver.server.Response;
+import ch.ethz.sis.afsserver.server.impl.ApiRequest;
+import ch.ethz.sis.afsserver.server.impl.ApiResponseBuilder;
+import ch.ethz.sis.afsserver.server.performance.PerformanceAuditor;
+import ch.ethz.sis.shared.log.LogManager;
+import ch.ethz.sis.shared.log.Logger;
+
+import java.util.Map;
+import java.util.UUID;
+
+public class PublicAPIAdapterWrapper extends AbstractPublicAPIWrapper {
+
+    private static final Logger logger = LogManager.getLogger(PublicAPIAdapterWrapper.class);
+
+    private APIServer apiServer;
+    private final ApiResponseBuilder apiResponseBuilder;
+
+    public PublicAPIAdapterWrapper(APIServer apiServerAdapter) {
+        this.apiServer = apiServerAdapter;
+        this.apiResponseBuilder = new ApiResponseBuilder();
+    }
+
+    public <E> E process(String method, Map<String, Object> args) {
+        PerformanceAuditor performanceAuditor = new PerformanceAuditor();
+        // Random Session token just works for tests with dummy authentication
+        ApiRequest request = new ApiRequest("test", method, args, UUID.randomUUID().toString(), null, null);
+
+        try {
+            Response response = apiServer.processOperation(request, apiResponseBuilder, performanceAuditor);
+            return (E) response.getResult();
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
+    }
+
+}
diff --git a/afs/build.gradle b/afs/build.gradle
index b8fb40aaa02..89f23edfc9a 100644
--- a/afs/build.gradle
+++ b/afs/build.gradle
@@ -1,6 +1,10 @@
 apply plugin: 'java'
 apply plugin: 'application'
 
+compileJava {
+    options.compilerArgs << '-parameters'
+}
+
 repositories {
     ivy {
         ivyPattern "https://sissource.ethz.ch/openbis/openbis-public/openbis-ivy/-/raw/main/[organisation]/[module]/[revision]/ivy.xml"
-- 
GitLab