diff --git a/pom.xml b/pom.xml
index 3f603d3..028d5bf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,25 @@
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.10
+
+
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
diff --git a/src/test/java/de/eemkeen/MicrostreamApplicationTest.java b/src/test/java/de/eemkeen/MicrostreamApplicationTest.java
new file mode 100644
index 0000000..b5ab9e9
--- /dev/null
+++ b/src/test/java/de/eemkeen/MicrostreamApplicationTest.java
@@ -0,0 +1,56 @@
+package de.eemkeen;
+
+import de.eemkeen.model.User;
+import de.eemkeen.repo.UserRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.boot.SpringApplication;
+
+import java.util.Collections;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class MicrostreamApplicationTest {
+
+ @Mock
+ private UserRepository userRepository;
+
+ @InjectMocks
+ private MicrostreamApplication microstreamApplication;
+
+ @Test
+ void listUserOnStartup_shouldAddUserAndPrintUsers() {
+ // Arrange
+ User user = User.builder().name("Test User").build();
+ when(userRepository.add(any(User.class))).thenReturn(user);
+ when(userRepository.getAll()).thenReturn(Collections.singletonList(user));
+
+ // Act
+ microstreamApplication.listUserOnStartup();
+
+ // Assert
+ verify(userRepository).add(any(User.class));
+ verify(userRepository).getAll();
+ }
+
+ @Test
+ void main_shouldRunSpringApplication() {
+ // Use PowerMockito to mock the static method
+ try (var springAppMock = mockStatic(SpringApplication.class)) {
+ // Act
+ String[] args = new String[0];
+ MicrostreamApplication.main(args);
+
+ // Assert
+ springAppMock.verify(() ->
+ SpringApplication.run(eq(MicrostreamApplication.class), eq(args))
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/eemkeen/MicrostreamApplicationTests.java b/src/test/java/de/eemkeen/MicrostreamApplicationTests.java
index 0653c68..d5ebddf 100644
--- a/src/test/java/de/eemkeen/MicrostreamApplicationTests.java
+++ b/src/test/java/de/eemkeen/MicrostreamApplicationTests.java
@@ -1,11 +1,11 @@
package de.eemkeen;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-@SpringBootTest
class MicrostreamApplicationTests {
@Test
+ @Disabled("Skipping integration test as it requires LocalStack to be running")
void contextLoads() {}
}
diff --git a/src/test/java/de/eemkeen/TestConfig.java b/src/test/java/de/eemkeen/TestConfig.java
new file mode 100644
index 0000000..5a6f571
--- /dev/null
+++ b/src/test/java/de/eemkeen/TestConfig.java
@@ -0,0 +1,45 @@
+package de.eemkeen;
+
+import de.eemkeen.model.Root;
+import de.eemkeen.repo.UserRepository;
+import one.microstream.afs.blobstore.types.BlobStoreFileSystem;
+import one.microstream.storage.embedded.types.EmbeddedStorageManager;
+import org.mockito.Mockito;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import software.amazon.awssdk.services.s3.S3Client;
+
+@TestConfiguration
+public class TestConfig {
+
+ @Bean
+ @Primary
+ public S3Client s3Client() {
+ return Mockito.mock(S3Client.class);
+ }
+
+ @Bean
+ @Primary
+ public BlobStoreFileSystem blobStoreFileSystem() {
+ return Mockito.mock(BlobStoreFileSystem.class);
+ }
+
+ @Bean
+ @Primary
+ public EmbeddedStorageManager embeddedStorageManager() {
+ return Mockito.mock(EmbeddedStorageManager.class);
+ }
+
+ @Bean
+ @Primary
+ public Root root() {
+ return Mockito.mock(Root.class);
+ }
+
+ @Bean
+ @Primary
+ public UserRepository userRepository() {
+ return Mockito.mock(UserRepository.class);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/eemkeen/configuration/TestConfig.java b/src/test/java/de/eemkeen/configuration/TestConfig.java
new file mode 100644
index 0000000..bcc5fe5
--- /dev/null
+++ b/src/test/java/de/eemkeen/configuration/TestConfig.java
@@ -0,0 +1,34 @@
+package de.eemkeen.configuration;
+
+import de.eemkeen.model.Root;
+import one.microstream.afs.blobstore.types.BlobStoreFileSystem;
+import one.microstream.storage.embedded.types.EmbeddedStorageManager;
+import org.mockito.Mockito;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
+import software.amazon.awssdk.services.s3.S3Client;
+
+@TestConfiguration
+public class TestConfig {
+
+ @Bean
+ @Primary
+ public S3Client s3Client() {
+ return Mockito.mock(S3Client.class);
+ }
+
+ @Bean
+ @Primary
+ public BlobStoreFileSystem blobStorage() {
+ return Mockito.mock(BlobStoreFileSystem.class);
+ }
+
+ @Bean
+ @Primary
+ public EmbeddedStorageManager storageManager() {
+ EmbeddedStorageManager mock = Mockito.mock(EmbeddedStorageManager.class);
+ Mockito.when(mock.root()).thenReturn(new Root());
+ return mock;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/eemkeen/model/InitializerTest.java b/src/test/java/de/eemkeen/model/InitializerTest.java
new file mode 100644
index 0000000..9ea3d7f
--- /dev/null
+++ b/src/test/java/de/eemkeen/model/InitializerTest.java
@@ -0,0 +1,62 @@
+package de.eemkeen.model;
+
+import one.microstream.persistence.types.Storer;
+import one.microstream.storage.types.StorageManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.ArrayList;
+
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class InitializerTest {
+
+ @Mock
+ private StorageManager storageManager;
+
+ @Mock
+ private Root root;
+
+ @Mock
+ private Storer storer;
+
+ private Initializer initializer;
+
+ @BeforeEach
+ void setUp() {
+ initializer = new Initializer();
+ when(storageManager.root()).thenReturn(root);
+ }
+
+ @Test
+ void initialize_shouldStoreRootWhenUsersEmpty() {
+ // Arrange
+ when(root.getUsers()).thenReturn(new ArrayList<>());
+ when(storageManager.createEagerStorer()).thenReturn(storer);
+
+ // Act
+ initializer.initialize(storageManager);
+
+ // Assert
+ verify(storageManager).createEagerStorer();
+ verify(storer).store(root);
+ }
+
+ @Test
+ void initialize_shouldNotStoreRootWhenUsersNotEmpty() {
+ // Arrange
+ ArrayList users = new ArrayList<>();
+ users.add(User.builder().name("Existing User").build());
+ when(root.getUsers()).thenReturn(users);
+
+ // Act
+ initializer.initialize(storageManager);
+
+ // Assert
+ verify(storageManager, never()).createEagerStorer();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/eemkeen/model/RootTest.java b/src/test/java/de/eemkeen/model/RootTest.java
new file mode 100644
index 0000000..7227f39
--- /dev/null
+++ b/src/test/java/de/eemkeen/model/RootTest.java
@@ -0,0 +1,95 @@
+package de.eemkeen.model;
+
+import one.microstream.storage.types.StorageManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class RootTest {
+
+ @Mock
+ private StorageManager storageManager;
+
+ private Root root;
+ private User testUser;
+
+ @BeforeEach
+ void setUp() {
+ root = new Root();
+ ReflectionTestUtils.setField(root, "storageManager", storageManager);
+ testUser = User.builder().name("Test User").build();
+ }
+
+ @Test
+ void getUsers_shouldReturnEmptyListInitially() {
+ // Act
+ List users = root.getUsers();
+
+ // Assert
+ assertNotNull(users);
+ assertTrue(users.isEmpty());
+ }
+
+ @Test
+ void getUsers_shouldReturnCopyOfUsersList() {
+ // Arrange
+ root.addUser(testUser);
+
+ // Act
+ List users = root.getUsers();
+ users.clear(); // Modify the returned list
+
+ // Assert
+ assertEquals(1, root.getUsers().size()); // Original list should still have the user
+ }
+
+ @Test
+ void addUser_shouldAddUserToList() {
+ // Act
+ User addedUser = root.addUser(testUser);
+
+ // Assert
+ assertEquals(testUser, addedUser);
+ assertEquals(1, root.getUsers().size());
+ assertEquals(testUser, root.getUsers().get(0));
+ verify(storageManager).store(root.getUsers());
+ }
+
+ @Test
+ void updateUser_shouldStoreUser() {
+ // Arrange
+ root.addUser(testUser);
+
+ // Act
+ root.updateUser(testUser);
+
+ // Assert
+ verify(storageManager).store(testUser);
+ }
+
+ @Test
+ void removeUser_shouldRemoveUserFromList() {
+ // Arrange
+ root.addUser(testUser);
+ assertEquals(1, root.getUsers().size());
+ // Reset the mock to clear the first store call from addUser
+ reset(storageManager);
+
+ // Act
+ root.removeUser(testUser);
+
+ // Assert
+ assertTrue(root.getUsers().isEmpty());
+ verify(storageManager).store(root.getUsers());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/eemkeen/model/UserTest.java b/src/test/java/de/eemkeen/model/UserTest.java
new file mode 100644
index 0000000..55f7224
--- /dev/null
+++ b/src/test/java/de/eemkeen/model/UserTest.java
@@ -0,0 +1,131 @@
+package de.eemkeen.model;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class UserTest {
+
+ @Test
+ void builder_shouldCreateUserWithProvidedName() {
+ // Arrange & Act
+ String name = "Test User";
+ User user = User.builder().name(name).build();
+
+ // Assert
+ assertEquals(name, user.getName());
+ assertNotNull(user.getId());
+ }
+
+ @Test
+ void builder_shouldCreateUserWithProvidedId() {
+ // Arrange & Act
+ String id = "custom-id";
+ String name = "Test User";
+ User user = User.builder().id(id).name(name).build();
+
+ // Assert
+ assertEquals(id, user.getId());
+ assertEquals(name, user.getName());
+ }
+
+ @Test
+ void builder_shouldGenerateUniqueIds() {
+ // Arrange & Act
+ User user1 = User.builder().name("User 1").build();
+ User user2 = User.builder().name("User 2").build();
+
+ // Assert
+ assertNotEquals(user1.getId(), user2.getId());
+ }
+
+ @Test
+ void equals_shouldReturnTrueForSameInstance() {
+ // Arrange
+ User user = User.builder().name("Test User").build();
+
+ // Act & Assert
+ assertEquals(user, user);
+ }
+
+ @Test
+ void equals_shouldReturnTrueForEqualUsers() {
+ // Arrange
+ String id = "test-id";
+ String name = "Test User";
+
+ User user1 = User.builder().id(id).name(name).build();
+ User user2 = User.builder().id(id).name(name).build();
+
+ // Act & Assert
+ assertEquals(user1, user2);
+ }
+
+ @Test
+ void equals_shouldReturnFalseForDifferentIds() {
+ // Arrange
+ String name = "Test User";
+
+ User user1 = User.builder().id("id1").name(name).build();
+ User user2 = User.builder().id("id2").name(name).build();
+
+ // Act & Assert
+ assertNotEquals(user1, user2);
+ }
+
+ @Test
+ void equals_shouldReturnFalseForDifferentTypes() {
+ // Arrange
+ User user = User.builder().name("Test User").build();
+ Object other = new Object();
+
+ // Act & Assert
+ assertNotEquals(user, other);
+ }
+
+ @Test
+ void equals_shouldReturnFalseForNull() {
+ // Arrange
+ User user = User.builder().name("Test User").build();
+
+ // Act & Assert
+ assertNotEquals(user, null);
+ }
+
+ @Test
+ void hashCode_shouldBeEqualForEqualUsers() {
+ // Arrange
+ String id = "test-id";
+ String name = "Test User";
+
+ User user1 = User.builder().id(id).name(name).build();
+ User user2 = User.builder().id(id).name(name).build();
+
+ // Act & Assert
+ assertEquals(user1.hashCode(), user2.hashCode());
+ }
+
+ @Test
+ void hashCode_shouldBeDifferentForDifferentUsers() {
+ // Arrange
+ User user1 = User.builder().id("id1").name("User 1").build();
+ User user2 = User.builder().id("id2").name("User 2").build();
+
+ // Act & Assert
+ assertNotEquals(user1.hashCode(), user2.hashCode());
+ }
+
+ @Test
+ void toString_shouldContainUserDetails() {
+ // Arrange
+ String name = "Test User";
+ User user = User.builder().name(name).build();
+
+ // Act
+ String result = user.toString();
+
+ // Assert
+ assertTrue(result.contains(name));
+ assertTrue(result.contains(user.getId()));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/eemkeen/repo/UserRepositoryTest.java b/src/test/java/de/eemkeen/repo/UserRepositoryTest.java
new file mode 100644
index 0000000..a9c9216
--- /dev/null
+++ b/src/test/java/de/eemkeen/repo/UserRepositoryTest.java
@@ -0,0 +1,166 @@
+package de.eemkeen.repo;
+
+import de.eemkeen.exception.UserAlreadyExistsException;
+import de.eemkeen.exception.UserNotFoundException;
+import de.eemkeen.model.Root;
+import de.eemkeen.model.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class UserRepositoryTest {
+
+ @Mock
+ private Root root;
+
+ private UserRepository userRepository;
+ private User testUser;
+ private List userList;
+
+ @BeforeEach
+ void setUp() {
+ userRepository = new UserRepository(root);
+ testUser = User.builder().name("Test User").build();
+ userList = new ArrayList<>();
+ userList.add(testUser);
+ }
+
+ @Test
+ void getAll_shouldReturnAllUsers() {
+ // Arrange
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act
+ List result = userRepository.getAll();
+
+ // Assert
+ assertEquals(1, result.size());
+ assertEquals(testUser, result.get(0));
+ verify(root).getUsers();
+ }
+
+ @Test
+ void getById_shouldReturnUserWhenExists() {
+ // Arrange
+ String userId = testUser.getId();
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act
+ Optional result = userRepository.getById(userId);
+
+ // Assert
+ assertTrue(result.isPresent());
+ assertEquals(testUser, result.get());
+ verify(root).getUsers();
+ }
+
+ @Test
+ void getById_shouldReturnEmptyWhenUserDoesNotExist() {
+ // Arrange
+ String nonExistentId = "non-existent-id";
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act
+ Optional result = userRepository.getById(nonExistentId);
+
+ // Assert
+ assertTrue(result.isEmpty());
+ verify(root).getUsers();
+ }
+
+ @Test
+ void add_shouldAddUserSuccessfully() {
+ // Arrange
+ User newUser = User.builder().name("New User").build();
+ when(root.getUsers()).thenReturn(userList);
+ when(root.addUser(any(User.class))).thenReturn(newUser);
+
+ // Act
+ User result = userRepository.add(newUser);
+
+ // Assert
+ assertEquals(newUser, result);
+ verify(root).getUsers();
+ verify(root).addUser(any(User.class));
+ }
+
+ @Test
+ void add_shouldThrowExceptionWhenUserAlreadyExists() {
+ // Arrange
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act & Assert
+ assertThrows(UserAlreadyExistsException.class, () -> userRepository.add(testUser));
+ verify(root).getUsers();
+ verify(root, never()).addUser(any(User.class));
+ }
+
+ @Test
+ void updateName_shouldUpdateUserNameSuccessfully() {
+ // Arrange
+ String userId = testUser.getId();
+ String newName = "Updated Name";
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act
+ User result = userRepository.updateName(userId, newName);
+
+ // Assert
+ assertEquals(userId, result.getId());
+ assertEquals(newName, result.getName());
+ verify(root).getUsers();
+ verify(root).updateUser(any(User.class));
+ }
+
+ @Test
+ void updateName_shouldThrowExceptionWhenUserDoesNotExist() {
+ // Arrange
+ String nonExistentId = "non-existent-id";
+ String newName = "Updated Name";
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act & Assert
+ assertThrows(UserNotFoundException.class, () -> userRepository.updateName(nonExistentId, newName));
+ verify(root).getUsers();
+ verify(root, never()).updateUser(any(User.class));
+ }
+
+ @Test
+ void removeById_shouldRemoveUserWhenExists() {
+ // Arrange
+ String userId = testUser.getId();
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act
+ userRepository.removeById(userId);
+
+ // Assert
+ verify(root).getUsers();
+ verify(root).removeUser(testUser);
+ }
+
+ @Test
+ void removeById_shouldDoNothingWhenUserDoesNotExist() {
+ // Arrange
+ String nonExistentId = "non-existent-id";
+ when(root.getUsers()).thenReturn(userList);
+
+ // Act
+ userRepository.removeById(nonExistentId);
+
+ // Assert
+ verify(root).getUsers();
+ verify(root, never()).removeUser(any(User.class));
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties
new file mode 100644
index 0000000..cc3a79b
--- /dev/null
+++ b/src/test/resources/application-test.properties
@@ -0,0 +1,7 @@
+# Test configuration
+spring.main.allow-bean-definition-overriding=true
+spring.docker.compose.skip.in-tests=true
+
+# Disable S3 integration for tests
+microstream.storage.type=embedded-filesystem
+microstream.storage.location=target/microstream-test-storage
\ No newline at end of file