diff --git a/pom.xml b/pom.xml index 428f705..619b7b7 100644 --- a/pom.xml +++ b/pom.xml @@ -301,6 +301,54 @@ + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + jacoco-check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.80 + + + + + + + + + + **/openapi/** + **/generated/** + **/SiteBackendApplication* + + + diff --git a/src/main/kotlin/be/sgl/backend/entity/setting/SettingId.kt b/src/main/kotlin/be/sgl/backend/entity/setting/SettingId.kt index 1f7ba62..aab1d59 100644 --- a/src/main/kotlin/be/sgl/backend/entity/setting/SettingId.kt +++ b/src/main/kotlin/be/sgl/backend/entity/setting/SettingId.kt @@ -5,5 +5,6 @@ enum class SettingId { REPRESENTATIVE_TITLE, REPRESENTATIVE_USERNAME, REPRESENTATIVE_SIGNATURE, - CALENDAR_NAME + CALENDAR_NAME, + ORGANIZATION_NAME } \ No newline at end of file diff --git a/src/test/kotlin/be/sgl/backend/alert/AlertLoggerTest.kt b/src/test/kotlin/be/sgl/backend/alert/AlertLoggerTest.kt new file mode 100644 index 0000000..38eca98 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/alert/AlertLoggerTest.kt @@ -0,0 +1,99 @@ +package be.sgl.backend.alert + +import be.sgl.backend.service.MailService +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.springframework.test.util.ReflectionTestUtils + +class AlertLoggerTest { + + @Mock + private lateinit var mailService: MailService + + @Mock + private lateinit var mailBuilder: MailService.MailBuilder + + @InjectMocks + private lateinit var alertLogger: AlertLogger + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + ReflectionTestUtils.setField(alertLogger, "enabled", true) + ReflectionTestUtils.setField(alertLogger, "mailRecipient", "admin@example.com") + ReflectionTestUtils.setField(alertLogger, "environment", "test") + ReflectionTestUtils.setField(alertLogger, "host", "https://test.example.com") + } + + @Test + fun `alert with lambda should send email when enabled`() { + `when`(mailService.builder()).thenReturn(mailBuilder) + `when`(mailBuilder.to(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.subject(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.body(anyString())).thenReturn(mailBuilder) + + alertLogger.alert(AlertCode.NEW_USER_EXISTS_NO_MEMBERSHIP) { "Test alert message" } + + verify(mailService).builder() + verify(mailBuilder).to("admin@example.com") + verify(mailBuilder).subject("https://test.example.com - test: NEW_USER_EXISTS_NO_MEMBERSHIP") + verify(mailBuilder).body("Test alert message") + verify(mailBuilder).send() + } + + @Test + fun `alert with string should send email when enabled`() { + `when`(mailService.builder()).thenReturn(mailBuilder) + `when`(mailBuilder.to(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.subject(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.body(anyString())).thenReturn(mailBuilder) + + alertLogger.alert(AlertCode.NEW_USER_EXISTS_PAID_MEMBERSHIP, "Test alert message") + + verify(mailService).builder() + verify(mailBuilder).to("admin@example.com") + verify(mailBuilder).subject("https://test.example.com - test: NEW_USER_EXISTS_PAID_MEMBERSHIP") + verify(mailBuilder).body("Test alert message") + verify(mailBuilder).send() + } + + @Test + fun `alert should not send email when disabled`() { + ReflectionTestUtils.setField(alertLogger, "enabled", false) + + alertLogger.alert(AlertCode.NEW_USER_EXISTS_NO_MEMBERSHIP, "Test alert message") + + verify(mailService, never()).builder() + } + + @Test + fun `alert should use correct mail recipient`() { + ReflectionTestUtils.setField(alertLogger, "mailRecipient", "custom@example.com") + `when`(mailService.builder()).thenReturn(mailBuilder) + `when`(mailBuilder.to(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.subject(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.body(anyString())).thenReturn(mailBuilder) + + alertLogger.alert(AlertCode.NEW_USER_EXISTS_NO_MEMBERSHIP, "Test alert") + + verify(mailBuilder).to("custom@example.com") + } + + @Test + fun `alert should format subject with host environment and code`() { + ReflectionTestUtils.setField(alertLogger, "host", "https://prod.example.com") + ReflectionTestUtils.setField(alertLogger, "environment", "production") + `when`(mailService.builder()).thenReturn(mailBuilder) + `when`(mailBuilder.to(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.subject(anyString())).thenReturn(mailBuilder) + `when`(mailBuilder.body(anyString())).thenReturn(mailBuilder) + + alertLogger.alert(AlertCode.NEW_USER_EXISTS_PAID_MEMBERSHIP, "Test") + + verify(mailBuilder).subject("https://prod.example.com - production: NEW_USER_EXISTS_PAID_MEMBERSHIP") + } +} diff --git a/src/test/kotlin/be/sgl/backend/config/CustomUserDetailsTest.kt b/src/test/kotlin/be/sgl/backend/config/CustomUserDetailsTest.kt new file mode 100644 index 0000000..2fe2e21 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/config/CustomUserDetailsTest.kt @@ -0,0 +1,166 @@ +package be.sgl.backend.config + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.jwt.Jwt + +class CustomUserDetailsTest { + + @Test + fun `CustomUserDetails should extract username from JWT`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertEquals("john.doe", userDetails.username) + } + + @Test + fun `CustomUserDetails should extract firstName from JWT`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertEquals("John", userDetails.firstName) + } + + @Test + fun `CustomUserDetails should extract lastName from JWT`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertEquals("Doe", userDetails.lastName) + } + + @Test + fun `CustomUserDetails should extract email from JWT`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertEquals("john.doe@example.com", userDetails.email) + } + + @Test + fun `CustomUserDetails should extract externalId from JWT`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertEquals("ext-12345", userDetails.externalId) + } + + @Test + fun `CustomUserDetails should extract authorities from JWT roles`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(listOf("ROLE_USER", "ROLE_ADMIN")) + + val userDetails = CustomUserDetails(jwt) + + val authorities = userDetails.authorities + assertEquals(2, authorities.size) + assertTrue(authorities.contains(SimpleGrantedAuthority("ROLE_USER"))) + assertTrue(authorities.contains(SimpleGrantedAuthority("ROLE_ADMIN"))) + } + + @Test + fun `CustomUserDetails should have empty authorities when roles claim is null`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + val authorities = userDetails.authorities + assertTrue(authorities.isEmpty()) + } + + @Test + fun `CustomUserDetails should have empty authorities when roles claim is empty list`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(emptyList()) + + val userDetails = CustomUserDetails(jwt) + + val authorities = userDetails.authorities + assertTrue(authorities.isEmpty()) + } + + @Test + fun `CustomUserDetails getPassword should return null`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertNull(userDetails.password) + } + + @Test + fun `CustomUserDetails should implement UserDetails interface`() { + val jwt = mock(Jwt::class.java) + `when`(jwt.getClaim("preferred_username")).thenReturn("john.doe") + `when`(jwt.getClaim("given_name")).thenReturn("John") + `when`(jwt.getClaim("family_name")).thenReturn("Doe") + `when`(jwt.getClaim("email")).thenReturn("john.doe@example.com") + `when`(jwt.getClaim("sub")).thenReturn("ext-12345") + `when`(jwt.getClaimAsStringList("roles")).thenReturn(null) + + val userDetails = CustomUserDetails(jwt) + + assertTrue(userDetails is org.springframework.security.core.userdetails.UserDetails) + } +} diff --git a/src/test/kotlin/be/sgl/backend/controller/BranchControllerIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/controller/BranchControllerIntegrationTest.kt new file mode 100644 index 0000000..4bfc8fe --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/controller/BranchControllerIntegrationTest.kt @@ -0,0 +1,271 @@ +package be.sgl.backend.controller + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.util.IntegrationTest +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.context.annotation.Import +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +@IntegrationTest +@AutoConfigureMockMvc +@Import(TestConfigurations::class) +@Transactional +class BranchControllerIntegrationTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Autowired + private lateinit var objectMapper: ObjectMapper + + private lateinit var activeBranch: Branch + private lateinit var passiveBranch: Branch + + @BeforeEach + fun setup() { + branchRepository.deleteAll() + + activeBranch = Branch().apply { + name = "Active Branch" + email = "active@example.com" + minimumAge = 6 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "Active Description" + law = "Law" + image = "active.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + activeBranch = branchRepository.save(activeBranch) + + passiveBranch = Branch().apply { + name = "Passive Branch" + email = "passive@example.com" + minimumAge = 12 + maximumAge = 16 + sex = Sex.UNKNOWN + description = "Passive Description" + law = "Law" + image = "passive.jpg" + status = BranchStatus.PASSIVE + staffTitle = "Leader" + } + passiveBranch = branchRepository.save(passiveBranch) + branchRepository.flush() + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun `GET all branches should return all branches for admin`() { + mockMvc.perform(get("/branches")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray) + .andExpect(jsonPath("$.length()").value(2)) + } + + @Test + fun `GET all branches should fail without authentication`() { + mockMvc.perform(get("/branches")) + .andExpect(status().isUnauthorized) + } + + @Test + @WithMockUser(roles = ["USER"]) + fun `GET all branches should fail for regular user`() { + mockMvc.perform(get("/branches")) + .andExpect(status().isForbidden) + } + + @Test + fun `GET visible branches should return only non-passive branches`() { + mockMvc.perform(get("/branches/visible")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray) + .andExpect(jsonPath("$[?(@.name == 'Active Branch')]").exists()) + .andExpect(jsonPath("$[?(@.name == 'Passive Branch')]").doesNotExist()) + } + + @Test + fun `GET branches with calendar should return only active branches`() { + mockMvc.perform(get("/branches/with-calendar")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray) + .andExpect(jsonPath("$[?(@.name == 'Active Branch')]").exists()) + } + + @Test + fun `GET branch by id should return branch`() { + mockMvc.perform(get("/branches/${activeBranch.id}")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.name").value("Active Branch")) + .andExpect(jsonPath("$.email").value("active@example.com")) + .andExpect(jsonPath("$.status").value("ACTIVE")) + } + + @Test + fun `GET branch by id should return 404 for non-existent branch`() { + mockMvc.perform(get("/branches/999")) + .andExpect(status().isNotFound) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `POST branch should create new branch`() { + val newBranch = mapOf( + "name" to "New Branch", + "email" to "new@example.com", + "minimumAge" to 16, + "maximumAge" to 18, + "sex" to "UNKNOWN", + "description" to "New Description", + "law" to "Law", + "status" to "ACTIVE", + "staffTitle" to "Leader" + ) + + mockMvc.perform( + post("/branches") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newBranch)) + ) + .andExpect(status().isCreated) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.name").value("New Branch")) + .andExpect(jsonPath("$.email").value("new@example.com")) + } + + @Test + fun `POST branch should fail without authentication`() { + val newBranch = mapOf( + "name" to "New Branch", + "email" to "new@example.com", + "minimumAge" to 16, + "maximumAge" to 18, + "sex" to "UNKNOWN", + "description" to "New Description", + "law" to "Law", + "status" to "ACTIVE", + "staffTitle" to "Leader" + ) + + mockMvc.perform( + post("/branches") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newBranch)) + ) + .andExpect(status().isUnauthorized) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `PUT branch should update existing branch`() { + val updatedBranch = mapOf( + "id" to activeBranch.id, + "name" to "Updated Branch", + "email" to "updated@example.com", + "minimumAge" to activeBranch.minimumAge, + "maximumAge" to activeBranch.maximumAge, + "sex" to activeBranch.sex.toString(), + "description" to "Updated Description", + "law" to activeBranch.law, + "status" to activeBranch.status.toString(), + "staffTitle" to activeBranch.staffTitle + ) + + mockMvc.perform( + put("/branches/${activeBranch.id}") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedBranch)) + ) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.name").value("Updated Branch")) + .andExpect(jsonPath("$.email").value("updated@example.com")) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `PUT branch should return 404 for non-existent branch`() { + val updatedBranch = mapOf( + "id" to 999, + "name" to "Updated Branch", + "email" to "updated@example.com", + "minimumAge" to 6, + "maximumAge" to 12, + "sex" to "UNKNOWN", + "description" to "Updated Description", + "law" to "Law", + "status" to "ACTIVE", + "staffTitle" to "Leader" + ) + + mockMvc.perform( + put("/branches/999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedBranch)) + ) + .andExpect(status().isNotFound) + } + + @Test + fun `PUT branch should fail without authentication`() { + val updatedBranch = mapOf( + "id" to activeBranch.id, + "name" to "Updated Branch", + "email" to "updated@example.com", + "minimumAge" to activeBranch.minimumAge, + "maximumAge" to activeBranch.maximumAge, + "sex" to activeBranch.sex.toString(), + "description" to "Updated Description", + "law" to activeBranch.law, + "status" to activeBranch.status.toString(), + "staffTitle" to activeBranch.staffTitle + ) + + mockMvc.perform( + put("/branches/${activeBranch.id}") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedBranch)) + ) + .andExpect(status().isUnauthorized) + } + + @Test + fun `GET visible branches should be publicly accessible`() { + mockMvc.perform(get("/branches/visible")) + .andExpect(status().isOk) + } + + @Test + fun `GET branch by id should be publicly accessible`() { + mockMvc.perform(get("/branches/${activeBranch.id}")) + .andExpect(status().isOk) + } + + @Test + fun `GET branches with calendar should be publicly accessible`() { + mockMvc.perform(get("/branches/with-calendar")) + .andExpect(status().isOk) + } +} diff --git a/src/test/kotlin/be/sgl/backend/controller/NewsItemControllerIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/controller/NewsItemControllerIntegrationTest.kt new file mode 100644 index 0000000..7d23ba0 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/controller/NewsItemControllerIntegrationTest.kt @@ -0,0 +1,300 @@ +package be.sgl.backend.controller + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.NewsItem +import be.sgl.backend.repository.NewsItemRepository +import be.sgl.backend.util.IntegrationTest +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.transaction.Transactional +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.context.annotation.Import +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +@IntegrationTest +@AutoConfigureMockMvc +@Import(TestConfigurations::class) +@Transactional +class NewsItemControllerIntegrationTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var newsItemRepository: NewsItemRepository + + @Autowired + private lateinit var objectMapper: ObjectMapper + + private lateinit var visibleNewsItem: NewsItem + private lateinit var hiddenNewsItem: NewsItem + + @BeforeEach + fun setup() { + newsItemRepository.deleteAll() + + visibleNewsItem = NewsItem().apply { + title = "Visible News" + content = "This is visible content" + visible = true + } + visibleNewsItem = newsItemRepository.save(visibleNewsItem) + + hiddenNewsItem = NewsItem().apply { + title = "Hidden News" + content = "This is hidden content" + visible = false + } + hiddenNewsItem = newsItemRepository.save(hiddenNewsItem) + newsItemRepository.flush() + } + + @Test + fun `GET visible news items should return only visible items`() { + mockMvc.perform(get("/news")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$").isArray) + .andExpect(jsonPath("$[?(@.title == 'Visible News')]").exists()) + .andExpect(jsonPath("$[?(@.title == 'Hidden News')]").doesNotExist()) + } + + @Test + fun `GET visible news items should be publicly accessible`() { + mockMvc.perform(get("/news")) + .andExpect(status().isOk) + } + + @Test + fun `GET news item by id should return news item`() { + mockMvc.perform(get("/news/${visibleNewsItem.id}")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.title").value("Visible News")) + .andExpect(jsonPath("$.content").value("This is visible content")) + .andExpect(jsonPath("$.visible").value(true)) + } + + @Test + fun `GET news item by id should return hidden items too`() { + mockMvc.perform(get("/news/${hiddenNewsItem.id}")) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.title").value("Hidden News")) + .andExpect(jsonPath("$.visible").value(false)) + } + + @Test + fun `GET news item by id should return 404 for non-existent item`() { + mockMvc.perform(get("/news/999")) + .andExpect(status().isNotFound) + } + + @Test + fun `GET news item by id should be publicly accessible`() { + mockMvc.perform(get("/news/${visibleNewsItem.id}")) + .andExpect(status().isOk) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `POST news item should create new item`() { + val newNewsItem = mapOf( + "title" to "New News", + "content" to "New content", + "visible" to true + ) + + mockMvc.perform( + post("/news") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newNewsItem)) + ) + .andExpect(status().isCreated) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.title").value("New News")) + .andExpect(jsonPath("$.content").value("New content")) + .andExpect(jsonPath("$.visible").value(true)) + } + + @Test + fun `POST news item should fail without authentication`() { + val newNewsItem = mapOf( + "title" to "New News", + "content" to "New content", + "visible" to true + ) + + mockMvc.perform( + post("/news") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newNewsItem)) + ) + .andExpect(status().isUnauthorized) + } + + @Test + @WithMockUser(roles = ["USER"]) + fun `POST news item should fail for regular user`() { + val newNewsItem = mapOf( + "title" to "New News", + "content" to "New content", + "visible" to true + ) + + mockMvc.perform( + post("/news") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newNewsItem)) + ) + .andExpect(status().isForbidden) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `PUT news item should update existing item`() { + val updatedNewsItem = mapOf( + "id" to visibleNewsItem.id, + "title" to "Updated News", + "content" to "Updated content", + "visible" to false + ) + + mockMvc.perform( + put("/news/${visibleNewsItem.id}") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedNewsItem)) + ) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.title").value("Updated News")) + .andExpect(jsonPath("$.content").value("Updated content")) + .andExpect(jsonPath("$.visible").value(false)) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `PUT news item should return 404 for non-existent item`() { + val updatedNewsItem = mapOf( + "id" to 999, + "title" to "Updated News", + "content" to "Updated content", + "visible" to true + ) + + mockMvc.perform( + put("/news/999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedNewsItem)) + ) + .andExpect(status().isNotFound) + } + + @Test + fun `PUT news item should fail without authentication`() { + val updatedNewsItem = mapOf( + "id" to visibleNewsItem.id, + "title" to "Updated News", + "content" to "Updated content", + "visible" to false + ) + + mockMvc.perform( + put("/news/${visibleNewsItem.id}") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updatedNewsItem)) + ) + .andExpect(status().isUnauthorized) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `DELETE news item should delete existing item`() { + mockMvc.perform(delete("/news/${visibleNewsItem.id}")) + .andExpect(status().isOk) + + // Verify deletion + mockMvc.perform(get("/news/${visibleNewsItem.id}")) + .andExpect(status().isNotFound) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `DELETE news item should return 404 for non-existent item`() { + mockMvc.perform(delete("/news/999")) + .andExpect(status().isNotFound) + } + + @Test + fun `DELETE news item should fail without authentication`() { + mockMvc.perform(delete("/news/${visibleNewsItem.id}")) + .andExpect(status().isUnauthorized) + } + + @Test + @WithMockUser(roles = ["USER"]) + fun `DELETE news item should fail for regular user`() { + mockMvc.perform(delete("/news/${visibleNewsItem.id}")) + .andExpect(status().isForbidden) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `POST news item should validate required fields`() { + val invalidNewsItem = mapOf( + "title" to "", // Empty title + "content" to "Some content", + "visible" to true + ) + + mockMvc.perform( + post("/news") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidNewsItem)) + ) + .andExpect(status().isBadRequest) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `news items should be ordered by most recent first`() { + // Create a newer news item + Thread.sleep(10) // Ensure different timestamps + val newerNewsItem = NewsItem().apply { + title = "Newer News" + content = "Newer content" + visible = true + } + newsItemRepository.save(newerNewsItem) + newsItemRepository.flush() + + mockMvc.perform(get("/news")) + .andExpect(status().isOk) + .andExpect(jsonPath("$[0].title").value("Newer News")) + } + + @Test + @WithMockUser(roles = ["STAFF"]) + fun `PUT news item can toggle visibility`() { + val toggledNewsItem = mapOf( + "id" to visibleNewsItem.id, + "title" to visibleNewsItem.title, + "content" to visibleNewsItem.content, + "visible" to false // Toggle from true to false + ) + + mockMvc.perform( + put("/news/${visibleNewsItem.id}") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(toggledNewsItem)) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.visible").value(false)) + } +} diff --git a/src/test/kotlin/be/sgl/backend/controller/SettingControllerIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/controller/SettingControllerIntegrationTest.kt new file mode 100644 index 0000000..fb09c04 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/controller/SettingControllerIntegrationTest.kt @@ -0,0 +1,153 @@ +package be.sgl.backend.controller + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.setting.Setting +import be.sgl.backend.entity.setting.SettingId +import be.sgl.backend.repository.SettingRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class SettingControllerIntegrationTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var settingRepository: SettingRepository + + @BeforeEach + fun setup() { + settingRepository.deleteAll() + } + + @Test + fun `GET setting should return setting value when it exists`() { + val setting = Setting(SettingId.CALENDAR_NAME, "Test Calendar") + settingRepository.save(setting) + settingRepository.flush() + + mockMvc.perform(get("/settings/${SettingId.CALENDAR_NAME.name}")) + .andExpect(status().isOk) + .andExpect(content().string("Test Calendar")) + } + + @Test + fun `GET setting should return empty when setting does not exist`() { + mockMvc.perform(get("/settings/${SettingId.CALENDAR_NAME.name}")) + .andExpect(status().isOk) + .andExpect(content().string("")) + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun `PUT setting should create new setting`() { + mockMvc.perform( + put("/settings/${SettingId.CALENDAR_NAME.name}") + .param("value", "New Calendar") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ) + .andExpect(status().isOk) + + val saved = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assert(saved != null) + assert(saved?.value == "New Calendar") + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun `PUT setting should update existing setting`() { + val setting = Setting(SettingId.CALENDAR_NAME, "Old Value") + settingRepository.save(setting) + settingRepository.flush() + + mockMvc.perform( + put("/settings/${SettingId.CALENDAR_NAME.name}") + .param("value", "Updated Value") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ) + .andExpect(status().isOk) + + val updated = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assert(updated?.value == "Updated Value") + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun `PUT setting with null value should delete setting`() { + val setting = Setting(SettingId.CALENDAR_NAME, "To Delete") + settingRepository.save(setting) + settingRepository.flush() + + mockMvc.perform( + put("/settings/${SettingId.CALENDAR_NAME.name}") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ) + .andExpect(status().isOk) + + val deleted = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assert(deleted == null) + } + + @Test + @WithMockUser(roles = ["USER"]) + fun `PUT setting should require ADMIN role`() { + mockMvc.perform( + put("/settings/${SettingId.CALENDAR_NAME.name}") + .param("value", "New Value") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ) + .andExpect(status().isForbidden) + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun `PUT setting should handle numeric values`() { + mockMvc.perform( + put("/settings/${SettingId.LATEST_DISPATCH_RATE.name}") + .param("value", "42.5") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ) + .andExpect(status().isOk) + + val saved = settingRepository.findByIdOrNull(SettingId.LATEST_DISPATCH_RATE.name) + assert(saved?.value == "42.5") + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun `PUT setting should handle empty string`() { + mockMvc.perform( + put("/settings/${SettingId.CALENDAR_NAME.name}") + .param("value", "") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + ) + .andExpect(status().isOk) + + val saved = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assert(saved?.value == "") + } + + @Test + fun `GET setting should be publicly accessible`() { + val setting = Setting(SettingId.CALENDAR_NAME, "Public Calendar") + settingRepository.save(setting) + settingRepository.flush() + + // No authentication + mockMvc.perform(get("/settings/${SettingId.CALENDAR_NAME.name}")) + .andExpect(status().isOk) + .andExpect(content().string("Public Calendar")) + } +} diff --git a/src/test/kotlin/be/sgl/backend/controller/SettingControllerTest.kt b/src/test/kotlin/be/sgl/backend/controller/SettingControllerTest.kt new file mode 100644 index 0000000..f668800 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/controller/SettingControllerTest.kt @@ -0,0 +1,101 @@ +package be.sgl.backend.controller + +import be.sgl.backend.entity.setting.SettingId +import be.sgl.backend.service.SettingService +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations +import org.springframework.http.HttpStatus + +class SettingControllerTest { + + @Mock + private lateinit var settingService: SettingService + + @InjectMocks + private lateinit var settingController: SettingController + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun `getSetting should return setting value when it exists`() { + val settingId = SettingId.CALENDAR_NAME + `when`(settingService.get(settingId)).thenReturn("Test Calendar") + + val response = settingController.getSetting(settingId) + + assertEquals(HttpStatus.OK, response.statusCode) + assertEquals("Test Calendar", response.body) + verify(settingService).get(settingId) + } + + @Test + fun `getSetting should return null when setting does not exist`() { + val settingId = SettingId.CALENDAR_NAME + `when`(settingService.get(settingId)).thenReturn(null) + + val response = settingController.getSetting(settingId) + + assertEquals(HttpStatus.OK, response.statusCode) + assertNull(response.body) + verify(settingService).get(settingId) + } + + @Test + fun `updateSetting should update setting value`() { + val settingId = SettingId.CALENDAR_NAME + val value = "New Calendar Name" + + val response = settingController.updateSetting(settingId, value) + + assertEquals(HttpStatus.OK, response.statusCode) + verify(settingService).update(settingId, value) + } + + @Test + fun `updateSetting should handle null value`() { + val settingId = SettingId.CALENDAR_NAME + + val response = settingController.updateSetting(settingId, null) + + assertEquals(HttpStatus.OK, response.statusCode) + verify(settingService).update(settingId, null) + } + + @Test + fun `updateSetting should handle empty value`() { + val settingId = SettingId.CALENDAR_NAME + val value = "" + + val response = settingController.updateSetting(settingId, value) + + assertEquals(HttpStatus.OK, response.statusCode) + verify(settingService).update(settingId, value) + } + + @Test + fun `updateSetting should handle multiple different setting IDs`() { + val settings = listOf( + SettingId.CALENDAR_NAME to "Calendar", + SettingId.ORGANIZATION_NAME to "Organization", + SettingId.REPRESENTATIVE_TITLE to "Title", + SettingId.REPRESENTATIVE_USERNAME to "Username" + ) + + settings.forEach { (id, value) -> + val response = settingController.updateSetting(id, value) + assertEquals(HttpStatus.OK, response.statusCode) + } + + settings.forEach { (id, value) -> + verify(settingService).update(id, value) + } + } +} diff --git a/src/test/kotlin/be/sgl/backend/entity/AddressTest.kt b/src/test/kotlin/be/sgl/backend/entity/AddressTest.kt new file mode 100644 index 0000000..76383d1 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/entity/AddressTest.kt @@ -0,0 +1,130 @@ +package be.sgl.backend.entity + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class AddressTest { + + @Test + fun `getStreetAddress should format street and number correctly`() { + val address = Address().apply { + street = "Main Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + assertEquals("Main Street 123", address.getStreetAdress()) + } + + @Test + fun `getStreetAddress should include subPremise when present`() { + val address = Address().apply { + street = "Main Street" + number = "123" + subPremise = "A" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + assertEquals("Main Street 123A", address.getStreetAdress()) + } + + @Test + fun `getStreetAddress should not include subPremise when null`() { + val address = Address().apply { + street = "Main Street" + number = "123" + subPremise = null + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + assertEquals("Main Street 123", address.getStreetAdress()) + } + + @Test + fun `toString should format complete address correctly`() { + val address = Address().apply { + street = "Main Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + assertEquals("Main Street 123, 1000 Brussels (BE)", address.toString()) + } + + @Test + fun `toString should format complete address with subPremise`() { + val address = Address().apply { + street = "Main Street" + number = "123" + subPremise = "B" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + assertEquals("Main Street 123B, 1000 Brussels (BE)", address.toString()) + } + + @Test + fun `postalAddress should default to false`() { + val address = Address().apply { + street = "Main Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + assertFalse(address.postalAdress) + } + + @Test + fun `postalAddress can be set to true`() { + val address = Address().apply { + street = "Main Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + postalAdress = true + } + + assertTrue(address.postalAdress) + } + + @Test + fun `externalId can be set and retrieved`() { + val address = Address().apply { + street = "Main Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + externalId = "EXT-12345" + } + + assertEquals("EXT-12345", address.externalId) + } + + @Test + fun `description can be set and retrieved`() { + val address = Address().apply { + street = "Main Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + description = "Main office" + } + + assertEquals("Main office", address.description) + } +} diff --git a/src/test/kotlin/be/sgl/backend/entity/MembershipPeriodTest.kt b/src/test/kotlin/be/sgl/backend/entity/MembershipPeriodTest.kt new file mode 100644 index 0000000..74dac3e --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/entity/MembershipPeriodTest.kt @@ -0,0 +1,283 @@ +package be.sgl.backend.entity + +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.membership.MembershipPeriod +import be.sgl.backend.entity.membership.MembershipRestriction +import be.sgl.backend.entity.user.Sex +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class MembershipPeriodTest { + + @Test + fun `getLimitForBranch should return limit when restriction exists for branch`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val restriction = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.registrationLimit = 50 + } + + period.restrictions.add(restriction) + + val limit = period.getLimitForBranch(branch) + + assertEquals(50, limit) + } + + @Test + fun `getLimitForBranch should return null when no restriction for branch`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val limit = period.getLimitForBranch(branch) + + assertNull(limit) + } + + @Test + fun `toString should format dates in Belgian format`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val result = period.toString() + + assertEquals("01/09/2024 - 31/08/2025", result) + } + + @Test + fun `validateRestrictions should pass when no restrictions`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + assertDoesNotThrow { + period.validateRestrictions() + } + } + + @Test + fun `validateRestrictions should pass with valid time restrictions`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val timeRestriction1 = MembershipRestriction().apply { + this.period = period + this.alternativeStart = LocalDate.of(2024, 7, 1) + this.alternativePrice = 80.0 + } + + val timeRestriction2 = MembershipRestriction().apply { + this.period = period + this.alternativeStart = LocalDate.of(2024, 8, 1) + this.alternativePrice = 90.0 + } + + period.restrictions.addAll(listOf(timeRestriction1, timeRestriction2)) + + assertDoesNotThrow { + period.validateRestrictions() + } + } + + @Test + fun `validateRestrictions should pass with one branch restriction per branch`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val branch1 = Branch().apply { + name = "Branch 1" + email = "branch1@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val branch2 = Branch().apply { + name = "Branch 2" + email = "branch2@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val restriction1 = MembershipRestriction().apply { + this.period = period + this.branch = branch1 + this.registrationLimit = 50 + } + + val restriction2 = MembershipRestriction().apply { + this.period = period + this.branch = branch2 + this.registrationLimit = 60 + } + + period.restrictions.addAll(listOf(restriction1, restriction2)) + + assertDoesNotThrow { + period.validateRestrictions() + } + } + + @Test + fun `validateRestrictions should fail with multiple non-time restrictions for same branch`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val restriction1 = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.registrationLimit = 50 + // No alternativeStart, so not a time restriction + } + + val restriction2 = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.alternativePrice = 75.0 + // No alternativeStart, so not a time restriction + } + + period.restrictions.addAll(listOf(restriction1, restriction2)) + + val exception = assertThrows(IllegalStateException::class.java) { + period.validateRestrictions() + } + + assertEquals("A branch should at most have one single non-time related restriction!", exception.message) + } + + @Test + fun `validateRestrictions should allow time restrictions plus one regular restriction per branch`() { + val period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + + val branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val branchRestriction = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.registrationLimit = 50 + } + + val timeRestriction1 = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.alternativeStart = LocalDate.of(2024, 7, 1) + this.alternativePrice = 80.0 + } + + val timeRestriction2 = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.alternativeStart = LocalDate.of(2024, 8, 1) + this.alternativePrice = 90.0 + } + + period.restrictions.addAll(listOf(branchRestriction, timeRestriction1, timeRestriction2)) + + assertDoesNotThrow { + period.validateRestrictions() + } + } + + @Test + fun `period should have default values`() { + val period = MembershipPeriod() + + assertEquals(0.0, period.price) + assertNull(period.registrationLimit) + assertEquals(3.0, period.reductionFactor) + assertEquals(0.0, period.siblingReduction) + assertNotNull(period.restrictions) + assertTrue(period.restrictions.isEmpty()) + } +} diff --git a/src/test/kotlin/be/sgl/backend/entity/PayableAndRegistrableTest.kt b/src/test/kotlin/be/sgl/backend/entity/PayableAndRegistrableTest.kt new file mode 100644 index 0000000..b806ddc --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/entity/PayableAndRegistrableTest.kt @@ -0,0 +1,198 @@ +package be.sgl.backend.entity + +import be.sgl.backend.entity.registrable.Registrable +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +class PayableAndRegistrableTest { + + private class TestPayable : Payable() { + init { + name = "Test Payable" + description = "Test description" + } + } + + private class TestRegistrable : Registrable() { + init { + name = "Test Event" + description = "Test description" + } + } + + @Test + fun `Payable should initialize with default dates`() { + val payable = TestPayable() + + assertNotNull(payable.open) + assertNotNull(payable.closed) + } + + @Test + fun `Payable should allow setting name and description`() { + val payable = TestPayable() + + assertEquals("Test Payable", payable.name) + assertEquals("Test description", payable.description) + } + + @Test + fun `Payable should allow setting open and closed dates`() { + val payable = TestPayable() + val openDate = LocalDateTime.of(2024, 1, 1, 0, 0) + val closedDate = LocalDateTime.of(2024, 12, 31, 23, 59) + + payable.open = openDate + payable.closed = closedDate + + assertEquals(openDate, payable.open) + assertEquals(closedDate, payable.closed) + } + + @Test + fun `Registrable should extend Payable`() { + val registrable = TestRegistrable() + + assertTrue(registrable is Payable) + assertNotNull(registrable.open) + assertNotNull(registrable.closed) + } + + @Test + fun `Registrable should have start and end dates`() { + val registrable = TestRegistrable() + + assertNotNull(registrable.start) + assertNotNull(registrable.end) + } + + @Test + fun `Registrable should allow setting price and limits`() { + val registrable = TestRegistrable() + + registrable.price = 50.0 + registrable.registrationLimit = 100 + + assertEquals(50.0, registrable.price) + assertEquals(100, registrable.registrationLimit) + } + + @Test + fun `Registrable should have default boolean flags`() { + val registrable = TestRegistrable() + + assertTrue(registrable.cancellable) + assertTrue(registrable.sendConfirmation) + assertFalse(registrable.sendCompleteConfirmation) + assertFalse(registrable.cancelled) + } + + @Test + fun `Registrable should allow setting boolean flags`() { + val registrable = TestRegistrable() + + registrable.cancellable = false + registrable.sendConfirmation = false + registrable.sendCompleteConfirmation = true + registrable.cancelled = true + + assertFalse(registrable.cancellable) + assertFalse(registrable.sendConfirmation) + assertTrue(registrable.sendCompleteConfirmation) + assertTrue(registrable.cancelled) + } + + @Test + fun `Registrable readAdditionalData should return 0 when no rule`() { + val registrable = TestRegistrable() + registrable.additionalFormRule = null + + val result = registrable.readAdditionalData("{\"field\":\"value\"}") + + assertEquals(0.0, result) + } + + @Test + fun `Registrable readAdditionalData should return 0 when no data`() { + val registrable = TestRegistrable() + registrable.additionalFormRule = "price + 10" + + val result = registrable.readAdditionalData(null) + + assertEquals(0.0, result) + } + + @Test + fun `Registrable readAdditionalData should evaluate JSONata expression`() { + val registrable = TestRegistrable() + registrable.additionalFormRule = "nights * 10" + + val result = registrable.readAdditionalData("{\"nights\":\"3\"}") + + assertEquals(30.0, result) + } + + @Test + fun `Registrable readAdditionalData should coerce negative results to 0`() { + val registrable = TestRegistrable() + registrable.additionalFormRule = "nights - 10" + + val result = registrable.readAdditionalData("{\"nights\":\"2\"}") + + assertEquals(0.0, result) + } + + @Test + fun `Registrable readAdditionalData should handle complex JSONata expressions`() { + val registrable = TestRegistrable() + registrable.additionalFormRule = "adults * 20 + children * 10" + + val result = registrable.readAdditionalData("{\"adults\":\"2\",\"children\":\"3\"}") + + assertEquals(70.0, result) + } + + @Test + fun `Registrable should allow setting address`() { + val registrable = TestRegistrable() + val address = Address().apply { + street = "Test Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + registrable.address = address + + assertNotNull(registrable.address) + assertEquals("Test Street", registrable.address?.street) + } + + @Test + fun `Registrable should allow setting communication options`() { + val registrable = TestRegistrable() + + registrable.communicationCC = "cc@example.com" + registrable.sendConfirmation = true + registrable.sendCompleteConfirmation = true + + assertEquals("cc@example.com", registrable.communicationCC) + assertTrue(registrable.sendConfirmation) + assertTrue(registrable.sendCompleteConfirmation) + } + + @Test + fun `Registrable should allow setting additional form and rule`() { + val registrable = TestRegistrable() + val formJson = "{\"fields\":[{\"name\":\"nights\",\"type\":\"number\"}]}" + val rule = "nights * 25" + + registrable.additionalForm = formJson + registrable.additionalFormRule = rule + + assertEquals(formJson, registrable.additionalForm) + assertEquals(rule, registrable.additionalFormRule) + } +} diff --git a/src/test/kotlin/be/sgl/backend/entity/PaymentTest.kt b/src/test/kotlin/be/sgl/backend/entity/PaymentTest.kt new file mode 100644 index 0000000..639fc1a --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/entity/PaymentTest.kt @@ -0,0 +1,71 @@ +package be.sgl.backend.entity + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class PaymentTest { + + private class TestPayment : Payment() { + override fun getDescription(): String = "Test Payment Description" + } + + @Test + fun `Payment should initialize with default values`() { + val payment = TestPayment() + + assertNull(payment.id) + assertFalse(payment.paid) + assertEquals(0.0, payment.price) + assertNull(payment.paymentId) + } + + @Test + fun `markPaid should set paid to true`() { + val payment = TestPayment() + assertFalse(payment.paid) + + payment.markPaid() + + assertTrue(payment.paid) + } + + @Test + fun `price can be set and retrieved`() { + val payment = TestPayment() + payment.price = 42.50 + + assertEquals(42.50, payment.price) + } + + @Test + fun `paymentId can be set and retrieved`() { + val payment = TestPayment() + payment.paymentId = "PAY-12345" + + assertEquals("PAY-12345", payment.paymentId) + } + + @Test + fun `getDescription should return correct description`() { + val payment = TestPayment() + + assertEquals("Test Payment Description", payment.getDescription()) + } + + @Test + fun `paid can be set directly`() { + val payment = TestPayment() + payment.paid = true + + assertTrue(payment.paid) + } + + @Test + fun `markPaid should be idempotent`() { + val payment = TestPayment() + payment.markPaid() + payment.markPaid() + + assertTrue(payment.paid) + } +} diff --git a/src/test/kotlin/be/sgl/backend/entity/RegistrableStatusTest.kt b/src/test/kotlin/be/sgl/backend/entity/RegistrableStatusTest.kt new file mode 100644 index 0000000..b18534c --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/entity/RegistrableStatusTest.kt @@ -0,0 +1,161 @@ +package be.sgl.backend.entity + +import be.sgl.backend.entity.registrable.Registrable +import be.sgl.backend.entity.registrable.RegistrableStatus +import be.sgl.backend.entity.registrable.RegistrableStatus.Companion.getStatus +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +class RegistrableStatusTest { + + private class TestRegistrable : Registrable() { + fun setCancelled(value: Boolean) { + this.cancelled = value + } + + fun setOpenTime(time: LocalDateTime) { + this.open = time + } + + fun setClosedTime(time: LocalDateTime) { + this.closed = time + } + + fun setStartTime(time: LocalDateTime) { + this.start = time + } + + fun setEndTime(time: LocalDateTime) { + this.end = time + } + } + + @Test + fun `RegistrableStatus should have all expected values`() { + val values = RegistrableStatus.values() + assertEquals(6, values.size) + assertTrue(values.contains(RegistrableStatus.NOT_YET_OPEN)) + assertTrue(values.contains(RegistrableStatus.REGISTRATIONS_OPENED)) + assertTrue(values.contains(RegistrableStatus.REGISTRATIONS_COMPLETED)) + assertTrue(values.contains(RegistrableStatus.STARTED)) + assertTrue(values.contains(RegistrableStatus.COMPLETED)) + assertTrue(values.contains(RegistrableStatus.CANCELLED)) + } + + @Test + fun `getStatus should return CANCELLED when registrable is cancelled`() { + val registrable = TestRegistrable() + registrable.setCancelled(true) + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.CANCELLED, status) + } + + @Test + fun `getStatus should return NOT_YET_OPEN when before open time`() { + val now = LocalDateTime.now() + val registrable = TestRegistrable().apply { + setCancelled(false) + setOpenTime(now.plusDays(1)) + setClosedTime(now.plusDays(2)) + setStartTime(now.plusDays(3)) + setEndTime(now.plusDays(4)) + } + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.NOT_YET_OPEN, status) + } + + @Test + fun `getStatus should return REGISTRATIONS_OPENED when between open and closed`() { + val now = LocalDateTime.now() + val registrable = TestRegistrable().apply { + setCancelled(false) + setOpenTime(now.minusDays(1)) + setClosedTime(now.plusDays(1)) + setStartTime(now.plusDays(2)) + setEndTime(now.plusDays(3)) + } + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.REGISTRATIONS_OPENED, status) + } + + @Test + fun `getStatus should return REGISTRATIONS_COMPLETED when between closed and start`() { + val now = LocalDateTime.now() + val registrable = TestRegistrable().apply { + setCancelled(false) + setOpenTime(now.minusDays(3)) + setClosedTime(now.minusDays(2)) + setStartTime(now.plusDays(1)) + setEndTime(now.plusDays(2)) + } + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.REGISTRATIONS_COMPLETED, status) + } + + @Test + fun `getStatus should return STARTED when between start and end`() { + val now = LocalDateTime.now() + val registrable = TestRegistrable().apply { + setCancelled(false) + setOpenTime(now.minusDays(4)) + setClosedTime(now.minusDays(3)) + setStartTime(now.minusDays(2)) + setEndTime(now.plusDays(1)) + } + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.STARTED, status) + } + + @Test + fun `getStatus should return COMPLETED when after end time`() { + val now = LocalDateTime.now() + val registrable = TestRegistrable().apply { + setCancelled(false) + setOpenTime(now.minusDays(5)) + setClosedTime(now.minusDays(4)) + setStartTime(now.minusDays(3)) + setEndTime(now.minusDays(2)) + } + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.COMPLETED, status) + } + + @Test + fun `getStatus should prioritize CANCELLED over other statuses`() { + val now = LocalDateTime.now() + val registrable = TestRegistrable().apply { + setCancelled(true) + setOpenTime(now.minusDays(1)) + setClosedTime(now.plusDays(1)) + setStartTime(now.plusDays(2)) + setEndTime(now.plusDays(3)) + } + + val status = registrable.getStatus() + + assertEquals(RegistrableStatus.CANCELLED, status) + } + + @Test + fun `RegistrableStatus valueOf should return correct status`() { + assertEquals(RegistrableStatus.NOT_YET_OPEN, RegistrableStatus.valueOf("NOT_YET_OPEN")) + assertEquals(RegistrableStatus.REGISTRATIONS_OPENED, RegistrableStatus.valueOf("REGISTRATIONS_OPENED")) + assertEquals(RegistrableStatus.REGISTRATIONS_COMPLETED, RegistrableStatus.valueOf("REGISTRATIONS_COMPLETED")) + assertEquals(RegistrableStatus.STARTED, RegistrableStatus.valueOf("STARTED")) + assertEquals(RegistrableStatus.COMPLETED, RegistrableStatus.valueOf("COMPLETED")) + assertEquals(RegistrableStatus.CANCELLED, RegistrableStatus.valueOf("CANCELLED")) + } +} diff --git a/src/test/kotlin/be/sgl/backend/entity/UserEntityTest.kt b/src/test/kotlin/be/sgl/backend/entity/UserEntityTest.kt new file mode 100644 index 0000000..2acd087 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/entity/UserEntityTest.kt @@ -0,0 +1,261 @@ +package be.sgl.backend.entity + +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class UserEntityTest { + + @Test + fun `getFullName should combine firstName and name`() { + val user = User().apply { + firstName = "John" + name = "Doe" + email = "john@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + assertEquals("John Doe", user.getFullName()) + } + + @Test + fun `getAge should calculate age correctly`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 6, 15) + } + + val age = user.getAge(LocalDate.of(2024, 6, 15)) + + assertEquals(24, age) + } + + @Test + fun `getAge should calculate age correctly before birthday`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 6, 15) + } + + val age = user.getAge(LocalDate.of(2024, 6, 14)) + + assertEquals(23, age) // Still 23, birthday is tomorrow + } + + @Test + fun `getAge should calculate age correctly after birthday`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 6, 15) + } + + val age = user.getAge(LocalDate.of(2024, 6, 16)) + + assertEquals(24, age) // Already 24, birthday was yesterday + } + + @Test + fun `getAge with current date should calculate current age`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.now().minusYears(25) + } + + val age = user.getAge() + + assertEquals(25, age) + } + + @Test + fun `getHomeAddress should return postal address`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + val homeAddress = Address().apply { + street = "Home Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + postalAdress = true + } + + val otherAddress = Address().apply { + street = "Other Street" + number = "456" + zipcode = "2000" + town = "Antwerp" + country = "BE" + postalAdress = false + } + + user.addresses.add(otherAddress) + user.addresses.add(homeAddress) + + val result = user.getHomeAddress() + + assertNotNull(result) + assertEquals("Home Street", result?.street) + assertEquals("123", result?.number) + } + + @Test + fun `getHomeAddress should return null when no postal address`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + val address = Address().apply { + street = "Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + postalAdress = false + } + + user.addresses.add(address) + + val result = user.getHomeAddress() + + assertNull(result) + } + + @Test + fun `user should have default values`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + assertNull(user.username) + assertNull(user.externalId) + assertNull(user.customerId) + assertNull(user.memberId) + assertEquals(Sex.UNKNOWN, user.sex) + assertEquals(0, user.ageDeviation) + assertFalse(user.hasReduction) + assertFalse(user.hasHandicap) + assertNotNull(user.addresses) + assertNotNull(user.contacts) + assertNotNull(user.roles) + } + + @Test + fun `user should allow setting all optional fields`() { + val user = User().apply { + username = "johndoe" + externalId = "ext-123" + customerId = "cust-456" + memberId = "mem-789" + firstName = "John" + name = "Doe" + email = "john@example.com" + birthdate = LocalDate.of(2000, 1, 1) + sex = Sex.MALE + ageDeviation = 1 + image = "profile.jpg" + mobile = "0123456789" + nis = "12345678901" + accountNo = "BE12345678901234" + hasReduction = true + hasHandicap = false + } + + assertEquals("johndoe", user.username) + assertEquals("ext-123", user.externalId) + assertEquals("cust-456", user.customerId) + assertEquals("mem-789", user.memberId) + assertEquals("John", user.firstName) + assertEquals("Doe", user.name) + assertEquals("john@example.com", user.email) + assertEquals(Sex.MALE, user.sex) + assertEquals(1, user.ageDeviation) + assertEquals("profile.jpg", user.image) + assertEquals("0123456789", user.mobile) + assertEquals("12345678901", user.nis) + assertEquals("BE12345678901234", user.accountNo) + assertTrue(user.hasReduction) + assertFalse(user.hasHandicap) + } + + @Test + fun `user addresses should be mutable list`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + val address1 = Address().apply { + street = "Street 1" + number = "1" + zipcode = "1000" + town = "City 1" + country = "BE" + } + + val address2 = Address().apply { + street = "Street 2" + number = "2" + zipcode = "2000" + town = "City 2" + country = "BE" + } + + user.addresses.add(address1) + user.addresses.add(address2) + + assertEquals(2, user.addresses.size) + assertEquals("Street 1", user.addresses[0].street) + assertEquals("Street 2", user.addresses[1].street) + } + + @Test + fun `user contacts should be mutable list`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + assertTrue(user.contacts.isEmpty()) + + // Contacts can be added + assertEquals(0, user.contacts.size) + } + + @Test + fun `staffData should be initialized`() { + val user = User().apply { + firstName = "Test" + name = "User" + email = "test@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + + assertNotNull(user.staffData) + assertEquals(user, user.staffData.user) + } +} diff --git a/src/test/kotlin/be/sgl/backend/mapper/MapperIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/mapper/MapperIntegrationTest.kt new file mode 100644 index 0000000..16cd18f --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/mapper/MapperIntegrationTest.kt @@ -0,0 +1,227 @@ +package be.sgl.backend.mapper + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.AddressDTO +import be.sgl.backend.dto.BranchDTO +import be.sgl.backend.entity.Address +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.util.IntegrationTest +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import + +@IntegrationTest +@Import(TestConfigurations::class) +class MapperIntegrationTest { + + @Autowired + private lateinit var addressMapper: AddressMapper + + @Autowired + private lateinit var branchMapper: BranchMapper + + @Test + fun `AddressMapper should map entity to DTO correctly`() { + val address = Address().apply { + street = "Test Street" + number = "123" + subPremise = "A" + zipcode = "1000" + town = "Brussels" + country = "BE" + description = "Main office" + } + + val dto = addressMapper.toDto(address) + + assertEquals("Test Street", dto.street) + assertEquals("123", dto.number) + assertEquals("A", dto.subPremise) + assertEquals("1000", dto.zipcode) + assertEquals("Brussels", dto.town) + assertEquals("BE", dto.country) + assertEquals("Main office", dto.description) + } + + @Test + fun `AddressMapper should map DTO to entity correctly`() { + val dto = AddressDTO( + id = null, + street = "DTO Street", + number = "456", + subPremise = "B", + zipcode = "2000", + town = "Antwerp", + country = "BE", + description = "Branch office" + ) + + val entity = addressMapper.toEntity(dto) + + assertEquals("DTO Street", entity.street) + assertEquals("456", entity.number) + assertEquals("B", entity.subPremise) + assertEquals("2000", entity.zipcode) + assertEquals("Antwerp", entity.town) + assertEquals("BE", entity.country) + assertEquals("Branch office", entity.description) + } + + @Test + fun `AddressMapper should handle null subPremise and description`() { + val address = Address().apply { + street = "Simple Street" + number = "1" + subPremise = null + zipcode = "3000" + town = "Leuven" + country = "BE" + description = null + } + + val dto = addressMapper.toDto(address) + + assertNull(dto.subPremise) + assertNull(dto.description) + + val backToEntity = addressMapper.toEntity(dto) + + assertNull(backToEntity.subPremise) + assertNull(backToEntity.description) + } + + @Test + fun `BranchMapper should map entity to DTO correctly`() { + val branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.MALE + description = "Test description" + law = "Test law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val dto = branchMapper.toDto(branch) + + assertEquals("Test Branch", dto.name) + assertEquals("test@example.com", dto.email) + assertEquals(6, dto.minimumAge) + assertEquals(8, dto.maximumAge) + assertEquals(Sex.MALE, dto.sex) + assertEquals("Test description", dto.description) + assertEquals("Test law", dto.law) + assertEquals("test.jpg", dto.image) + assertEquals(BranchStatus.ACTIVE, dto.status) + assertEquals("Leader", dto.staffTitle) + } + + @Test + fun `BranchMapper should map DTO to entity correctly`() { + val dto = BranchDTO( + id = null, + name = "DTO Branch", + email = "dto@example.com", + minimumAge = 9, + maximumAge = 11, + sex = Sex.FEMALE, + description = "DTO description", + law = "DTO law", + image = "dto.jpg", + status = BranchStatus.MEMBER, + staffTitle = "Coach", + staff = emptyList() + ) + + val entity = branchMapper.toEntity(dto) + + assertEquals("DTO Branch", entity.name) + assertEquals("dto@example.com", entity.email) + assertEquals(9, entity.minimumAge) + assertEquals(11, entity.maximumAge) + assertEquals(Sex.FEMALE, entity.sex) + assertEquals("DTO description", entity.description) + assertEquals("DTO law", entity.law) + assertEquals("dto.jpg", entity.image) + assertEquals(BranchStatus.MEMBER, entity.status) + assertEquals("Coach", entity.staffTitle) + } + + @Test + fun `BranchMapper toBaseDto should map correctly`() { + val branch = Branch().apply { + name = "Base Branch" + email = "base@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Base description" + law = "Base law" + image = "base.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val baseDto = branchMapper.toBaseDto(branch) + + assertEquals("Base Branch", baseDto.name) + assertEquals("Base description", baseDto.description) + assertEquals("base.jpg", baseDto.image) + assertEquals(6, baseDto.minimumAge) + assertEquals(8, baseDto.maximumAge) + assertEquals(Sex.UNKNOWN, baseDto.sex) + } + + @Test + fun `mappers should be thread-safe for concurrent operations`() { + val addresses = (1..10).map { i -> + Address().apply { + street = "Street $i" + number = "$i" + zipcode = "${1000 + i}" + town = "Town $i" + country = "BE" + } + } + + val dtos = addresses.parallelStream() + .map { addressMapper.toDto(it) } + .toList() + + assertEquals(10, dtos.size) + dtos.forEachIndexed { index, dto -> + assertEquals("Street ${index + 1}", dto.street) + assertEquals("${index + 1}", dto.number) + } + } + + @Test + fun `mappers should preserve data integrity in round-trip conversion`() { + val originalAddress = Address().apply { + street = "Round Trip Street" + number = "999" + subPremise = "Z" + zipcode = "9999" + town = "Round Trip City" + country = "BE" + description = "Round trip test" + } + + val dto = addressMapper.toDto(originalAddress) + val backToEntity = addressMapper.toEntity(dto) + + assertEquals(originalAddress.street, backToEntity.street) + assertEquals(originalAddress.number, backToEntity.number) + assertEquals(originalAddress.subPremise, backToEntity.subPremise) + assertEquals(originalAddress.zipcode, backToEntity.zipcode) + assertEquals(originalAddress.town, backToEntity.town) + assertEquals(originalAddress.country, backToEntity.country) + assertEquals(originalAddress.description, backToEntity.description) + } +} diff --git a/src/test/kotlin/be/sgl/backend/repository/BranchRepositoryIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/repository/BranchRepositoryIntegrationTest.kt new file mode 100644 index 0000000..c256dc7 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/repository/BranchRepositoryIntegrationTest.kt @@ -0,0 +1,330 @@ +package be.sgl.backend.repository + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.calendar.Calendar +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.repository.calendar.CalendarRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class BranchRepositoryIntegrationTest { + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Autowired + private lateinit var calendarRepository: CalendarRepository + + @BeforeEach + fun setup() { + branchRepository.deleteAll() + calendarRepository.deleteAll() + } + + @Test + fun `getBranchesWithCalendar should return branches that have calendars`() { + val branchWithCalendar = Branch().apply { + name = "Branch with Calendar" + email = "with@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Has calendar" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + val savedBranch = branchRepository.save(branchWithCalendar) + + val calendar = Calendar().apply { + name = "Test Calendar" + branch = savedBranch + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + calendarRepository.save(calendar) + calendarRepository.flush() + + val branchWithoutCalendar = Branch().apply { + name = "Branch without Calendar" + email = "without@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "No calendar" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(branchWithoutCalendar) + branchRepository.flush() + + val branches = branchRepository.getBranchesWithCalendar() + + assertTrue(branches.any { it.name == "Branch with Calendar" }) + assertFalse(branches.any { it.name == "Branch without Calendar" }) + } + + @Test + fun `getVisibleBranches should return non-hidden branches`() { + val activeBranch = Branch().apply { + name = "Active Branch" + email = "active@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Active" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val memberBranch = Branch().apply { + name = "Member Branch" + email = "member@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Member" + law = "Law" + image = "test.jpg" + status = BranchStatus.MEMBER + staffTitle = "Leader" + } + + val passiveBranch = Branch().apply { + name = "Passive Branch" + email = "passive@example.com" + minimumAge = 12 + maximumAge = 14 + sex = Sex.UNKNOWN + description = "Passive" + law = "Law" + image = "test.jpg" + status = BranchStatus.PASSIVE + staffTitle = "Leader" + } + + val hiddenBranch = Branch().apply { + name = "Hidden Branch" + email = "hidden@example.com" + minimumAge = 15 + maximumAge = 17 + sex = Sex.UNKNOWN + description = "Hidden" + law = "Law" + image = "test.jpg" + status = BranchStatus.HIDDEN + staffTitle = "Leader" + } + + branchRepository.save(activeBranch) + branchRepository.save(memberBranch) + branchRepository.save(passiveBranch) + branchRepository.save(hiddenBranch) + branchRepository.flush() + + val visibleBranches = branchRepository.getVisibleBranches() + + assertTrue(visibleBranches.any { it.name == "Active Branch" }) + assertTrue(visibleBranches.any { it.name == "Member Branch" }) + assertTrue(visibleBranches.any { it.name == "Passive Branch" }) + assertFalse(visibleBranches.any { it.name == "Hidden Branch" }) + } + + @Test + fun `getPossibleBranchesForSexAndAge should match age range exactly`() { + val branch = Branch().apply { + name = "Exact Age Branch" + email = "exact@example.com" + minimumAge = 10 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "Ages 10-12" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(branch) + branchRepository.flush() + + val matchesMin = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 10) + val matchesMax = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 12) + val matchesMiddle = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 11) + val tooYoung = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 9) + val tooOld = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 13) + + assertTrue(matchesMin.any { it.name == "Exact Age Branch" }) + assertTrue(matchesMax.any { it.name == "Exact Age Branch" }) + assertTrue(matchesMiddle.any { it.name == "Exact Age Branch" }) + assertFalse(tooYoung.any { it.name == "Exact Age Branch" }) + assertFalse(tooOld.any { it.name == "Exact Age Branch" }) + } + + @Test + fun `getPossibleBranchesForSexAndAge should respect sex restrictions`() { + val boysBranch = Branch().apply { + name = "Boys Only" + email = "boys@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.MALE + description = "For boys" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val girlsBranch = Branch().apply { + name = "Girls Only" + email = "girls@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.FEMALE + description = "For girls" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val mixedBranch = Branch().apply { + name = "Mixed" + email = "mixed@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "For all" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + branchRepository.save(boysBranch) + branchRepository.save(girlsBranch) + branchRepository.save(mixedBranch) + branchRepository.flush() + + val boysMatches = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 7) + val girlsMatches = branchRepository.getPossibleBranchesForSexAndAge(Sex.FEMALE, 7) + + // Boys should match boys branch and mixed branch + assertTrue(boysMatches.any { it.name == "Boys Only" }) + assertFalse(boysMatches.any { it.name == "Girls Only" }) + assertTrue(boysMatches.any { it.name == "Mixed" }) + + // Girls should match girls branch and mixed branch + assertFalse(girlsMatches.any { it.name == "Boys Only" }) + assertTrue(girlsMatches.any { it.name == "Girls Only" }) + assertTrue(girlsMatches.any { it.name == "Mixed" }) + } + + @Test + fun `getPossibleBranchesForSexAndAge should only return active branches`() { + val activeBranch = Branch().apply { + name = "Active" + email = "active@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Active" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val passiveBranch = Branch().apply { + name = "Passive" + email = "passive@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Passive" + law = "Law" + image = "test.jpg" + status = BranchStatus.PASSIVE + staffTitle = "Leader" + } + + branchRepository.save(activeBranch) + branchRepository.save(passiveBranch) + branchRepository.flush() + + val matches = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 7) + + assertTrue(matches.any { it.name == "Active" }) + assertFalse(matches.any { it.name == "Passive" }) + } + + @Test + fun `getPossibleBranchesForSexAndAge should return empty list when no matches`() { + val branch = Branch().apply { + name = "Teenage Branch" + email = "teen@example.com" + minimumAge = 13 + maximumAge = 17 + sex = Sex.UNKNOWN + description = "Teens" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(branch) + branchRepository.flush() + + val matches = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 5) + + assertTrue(matches.isEmpty()) + } + + @Test + fun `branches should be persistable with all properties`() { + val branch = Branch().apply { + name = "Complete Branch" + email = "complete@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.MALE + description = "Complete description" + law = "Complete law text" + image = "complete.jpg" + status = BranchStatus.MEMBER + staffTitle = "Head Leader" + } + + val saved = branchRepository.save(branch) + branchRepository.flush() + + val retrieved = branchRepository.findById(saved.id!!).get() + + assertEquals("Complete Branch", retrieved.name) + assertEquals("complete@example.com", retrieved.email) + assertEquals(6, retrieved.minimumAge) + assertEquals(8, retrieved.maximumAge) + assertEquals(Sex.MALE, retrieved.sex) + assertEquals("Complete description", retrieved.description) + assertEquals("Complete law text", retrieved.law) + assertEquals("complete.jpg", retrieved.image) + assertEquals(BranchStatus.MEMBER, retrieved.status) + assertEquals("Head Leader", retrieved.staffTitle) + } +} diff --git a/src/test/kotlin/be/sgl/backend/repository/MembershipRepositoryIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/repository/MembershipRepositoryIntegrationTest.kt new file mode 100644 index 0000000..35df864 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/repository/MembershipRepositoryIntegrationTest.kt @@ -0,0 +1,276 @@ +package be.sgl.backend.repository + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.membership.Membership +import be.sgl.backend.entity.membership.MembershipPeriod +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.membership.MembershipPeriodRepository +import be.sgl.backend.repository.membership.MembershipRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class MembershipRepositoryIntegrationTest { + + @Autowired + private lateinit var membershipRepository: MembershipRepository + + @Autowired + private lateinit var membershipPeriodRepository: MembershipPeriodRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + private lateinit var testPeriod: MembershipPeriod + private lateinit var testBranch: Branch + private lateinit var testUser: User + + @BeforeEach + fun setup() { + membershipRepository.deleteAll() + membershipPeriodRepository.deleteAll() + branchRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Test law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + + testPeriod = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + } + testPeriod = membershipPeriodRepository.save(testPeriod) + + testUser = User().apply { + name = "Test" + firstName = "User" + email = "test@example.com" + birthdate = LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + } + + @Test + fun `countByPeriod should return correct count`() { + // Create 3 memberships for the period + repeat(3) { index -> + val user = User().apply { + name = "User$index" + firstName = "Test" + email = "user$index@example.com" + birthdate = LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + val membership = Membership(user, testPeriod, testBranch, 100.0) + membershipRepository.save(membership) + } + membershipRepository.flush() + + val count = membershipRepository.countByPeriod(testPeriod) + + assertEquals(3, count) + } + + @Test + fun `countByPeriodAndBranch should return correct count for specific branch`() { + val otherBranch = Branch().apply { + name = "Other Branch" + email = "other@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Other" + law = "Other law" + image = "other.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + otherBranch = branchRepository.save(otherBranch) + + // Create 2 memberships for test branch + repeat(2) { index -> + val user = User().apply { + name = "User$index" + firstName = "Test" + email = "user$index@example.com" + birthdate = LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + val membership = Membership(user, testPeriod, testBranch, 100.0) + membershipRepository.save(membership) + } + + // Create 1 membership for other branch + val otherUser = User().apply { + name = "OtherUser" + firstName = "Test" + email = "other@example.com" + birthdate = LocalDate.of(2005, 1, 1) + sex = Sex.MALE + } + membershipRepository.save(Membership(otherUser, testPeriod, otherBranch, 100.0)) + membershipRepository.flush() + + val count = membershipRepository.countByPeriodAndBranch(testPeriod, testBranch) + + assertEquals(2, count) + } + + @Test + fun `getMembershipsByUser should return all memberships for user`() { + val membership1 = Membership(testUser, testPeriod, testBranch, 100.0) + membershipRepository.save(membership1) + + val oldPeriod = MembershipPeriod().apply { + start = LocalDate.of(2023, 9, 1) + end = LocalDate.of(2024, 8, 31) + price = 90.0 + } + membershipPeriodRepository.save(oldPeriod) + + val membership2 = Membership(testUser, oldPeriod, testBranch, 90.0) + membershipRepository.save(membership2) + membershipRepository.flush() + + val memberships = membershipRepository.getMembershipsByUser(testUser) + + assertEquals(2, memberships.size) + assertTrue(memberships.any { it.period.id == testPeriod.id }) + assertTrue(memberships.any { it.period.id == oldPeriod.id }) + } + + @Test + fun `getCurrentByUser should return only current period membership`() { + // Set test period as current + testPeriod.start = LocalDate.now().minusMonths(2) + testPeriod.end = LocalDate.now().plusMonths(10) + membershipPeriodRepository.save(testPeriod) + + val membership = Membership(testUser, testPeriod, testBranch, 100.0) + membershipRepository.save(membership) + membershipRepository.flush() + + val currentMembership = membershipRepository.getCurrentByUser(testUser) + + assertNotNull(currentMembership) + assertEquals(testPeriod.id, currentMembership?.period?.id) + } + + @Test + fun `getCurrentByBranch should return current memberships for branch`() { + testPeriod.start = LocalDate.now().minusMonths(2) + testPeriod.end = LocalDate.now().plusMonths(10) + membershipPeriodRepository.save(testPeriod) + + repeat(3) { index -> + val user = User().apply { + name = "User$index" + firstName = "Test" + email = "user$index@example.com" + birthdate = LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + val membership = Membership(user, testPeriod, testBranch, 100.0) + membershipRepository.save(membership) + } + membershipRepository.flush() + + val currentMemberships = membershipRepository.getCurrentByBranch(testBranch) + + assertEquals(3, currentMemberships.size) + assertTrue(currentMemberships.all { it.branch.id == testBranch.id }) + } + + @Test + fun `existsByPeriodAndUser should return true when membership exists`() { + val membership = Membership(testUser, testPeriod, testBranch, 100.0) + membershipRepository.save(membership) + membershipRepository.flush() + + val exists = membershipRepository.existsByPeriodAndUser(testPeriod, testUser) + + assertTrue(exists) + } + + @Test + fun `existsByPeriodAndUser should return false when membership does not exist`() { + val otherUser = User().apply { + name = "Other" + firstName = "User" + email = "other@example.com" + birthdate = LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + + val exists = membershipRepository.existsByPeriodAndUser(testPeriod, otherUser) + + assertFalse(exists) + } + + @Test + fun `getCurrent should return all current memberships`() { + testPeriod.start = LocalDate.now().minusMonths(2) + testPeriod.end = LocalDate.now().plusMonths(10) + membershipPeriodRepository.save(testPeriod) + + val otherBranch = Branch().apply { + name = "Other Branch" + email = "other@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Other" + law = "Other law" + image = "other.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(otherBranch) + + // Create memberships for different branches + val user1 = User().apply { + name = "User1" + firstName = "Test" + email = "user1@example.com" + birthdate = LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + membershipRepository.save(Membership(user1, testPeriod, testBranch, 100.0)) + + val user2 = User().apply { + name = "User2" + firstName = "Test" + email = "user2@example.com" + birthdate = LocalDate.of(2005, 1, 1) + sex = Sex.MALE + } + membershipRepository.save(Membership(user2, testPeriod, otherBranch, 100.0)) + membershipRepository.flush() + + val currentMemberships = membershipRepository.getCurrent() + + assertEquals(2, currentMemberships.size) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/BranchServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/BranchServiceIntegrationTest.kt new file mode 100644 index 0000000..c4b9a07 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/BranchServiceIntegrationTest.kt @@ -0,0 +1,282 @@ +package be.sgl.backend.service + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.BranchDTO +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class BranchServiceIntegrationTest { + + @Autowired + private lateinit var branchService: BranchService + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Test + fun `getAllBranches should return all branches`() { + val branch1 = Branch().apply { + name = "Branch 1" + email = "branch1@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test 1" + law = "Law 1" + image = "test1.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val branch2 = Branch().apply { + name = "Branch 2" + email = "branch2@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Test 2" + law = "Law 2" + image = "test2.jpg" + status = BranchStatus.HIDDEN + staffTitle = "Coach" + } + + branchRepository.save(branch1) + branchRepository.save(branch2) + branchRepository.flush() + + val branches = branchService.getAllBranches() + + assertTrue(branches.size >= 2) + assertTrue(branches.any { it.name == "Branch 1" }) + assertTrue(branches.any { it.name == "Branch 2" }) + } + + @Test + fun `getVisibleBranches should only return non-hidden branches`() { + val visibleBranch = Branch().apply { + name = "Visible Branch" + email = "visible@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Visible" + law = "Law" + image = "visible.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val hiddenBranch = Branch().apply { + name = "Hidden Branch" + email = "hidden@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Hidden" + law = "Law" + image = "hidden.jpg" + status = BranchStatus.HIDDEN + staffTitle = "Coach" + } + + branchRepository.save(visibleBranch) + branchRepository.save(hiddenBranch) + branchRepository.flush() + + val branches = branchService.getVisibleBranches() + + assertTrue(branches.any { it.name == "Visible Branch" }) + assertFalse(branches.any { it.name == "Hidden Branch" }) + } + + @Test + fun `getBranchDTOById should return branch with staff`() { + val branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + val savedBranch = branchRepository.save(branch) + branchRepository.flush() + + val branchDTO = branchService.getBranchDTOById(savedBranch.id!!) + + assertNotNull(branchDTO) + assertEquals("Test Branch", branchDTO.name) + assertEquals("test@example.com", branchDTO.email) + assertEquals(6, branchDTO.minimumAge) + assertEquals(8, branchDTO.maximumAge) + } + + @Test + fun `mergeBranchDTOChanges should update branch properties`() { + val branch = Branch().apply { + name = "Original Branch" + email = "original@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.MALE + description = "Original description" + law = "Original law" + image = "original.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + val savedBranch = branchRepository.save(branch) + branchRepository.flush() + + val updatedDTO = BranchDTO( + id = savedBranch.id, + name = "Updated Branch", + email = "updated@example.com", + minimumAge = 7, + maximumAge = 9, + sex = Sex.FEMALE, + description = "Updated description", + law = "Updated law", + image = "original.jpg", + status = BranchStatus.PASSIVE, + staffTitle = "Coach", + staff = emptyList() + ) + + val result = branchService.mergeBranchDTOChanges(savedBranch.id!!, updatedDTO) + + assertEquals("Updated Branch", result.name) + assertEquals("updated@example.com", result.email) + assertEquals(7, result.minimumAge) + assertEquals(9, result.maximumAge) + assertEquals(Sex.FEMALE, result.sex) + assertEquals(BranchStatus.PASSIVE, result.status) + } + + @Test + fun `getPossibleBranchesForSexAndAge should return matching branches`() { + val branch1 = Branch().apply { + name = "Young Boys" + email = "young@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.MALE + description = "For young boys" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + val branch2 = Branch().apply { + name = "Older Girls" + email = "older@example.com" + minimumAge = 12 + maximumAge = 15 + sex = Sex.FEMALE + description = "For older girls" + law = "Law" + image = "test2.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + branchRepository.save(branch1) + branchRepository.save(branch2) + branchRepository.flush() + + val matchingBranches = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 7) + + assertEquals(1, matchingBranches.size) + assertEquals("Young Boys", matchingBranches[0].name) + } + + @Test + fun `getPossibleBranchesForSexAndAge should include UNKNOWN sex branches`() { + val mixedBranch = Branch().apply { + name = "Mixed Branch" + email = "mixed@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "For all" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + branchRepository.save(mixedBranch) + branchRepository.flush() + + val matchingBoys = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 7) + val matchingGirls = branchRepository.getPossibleBranchesForSexAndAge(Sex.FEMALE, 7) + + assertTrue(matchingBoys.any { it.name == "Mixed Branch" }) + assertTrue(matchingGirls.any { it.name == "Mixed Branch" }) + } + + @Test + fun `branch should not match when age is outside range`() { + val branch = Branch().apply { + name = "Specific Age Branch" + email = "age@example.com" + minimumAge = 10 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "For specific ages" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + + branchRepository.save(branch) + branchRepository.flush() + + val tooYoung = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 8) + val tooOld = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 15) + + assertFalse(tooYoung.any { it.name == "Specific Age Branch" }) + assertFalse(tooOld.any { it.name == "Specific Age Branch" }) + } + + @Test + fun `hidden branches should not be returned for matching`() { + val hiddenBranch = Branch().apply { + name = "Hidden Branch" + email = "hidden@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Hidden" + law = "Law" + image = "test.jpg" + status = BranchStatus.HIDDEN + staffTitle = "Leader" + } + + branchRepository.save(hiddenBranch) + branchRepository.flush() + + val matching = branchRepository.getPossibleBranchesForSexAndAge(Sex.MALE, 7) + + assertFalse(matching.any { it.name == "Hidden Branch" }) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/CalendarServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/CalendarServiceIntegrationTest.kt new file mode 100644 index 0000000..6d45054 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/CalendarServiceIntegrationTest.kt @@ -0,0 +1,364 @@ +package be.sgl.backend.service + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.CalendarPeriodDTO +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.calendar.Calendar +import be.sgl.backend.entity.calendar.CalendarItem +import be.sgl.backend.entity.calendar.CalendarPeriod +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.calendar.CalendarItemRepository +import be.sgl.backend.repository.calendar.CalendarPeriodRepository +import be.sgl.backend.repository.calendar.CalendarRepository +import be.sgl.backend.service.exception.CalendarPeriodNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.DayOfWeek +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class CalendarServiceIntegrationTest { + + @Autowired + private lateinit var calendarService: CalendarService + + @Autowired + private lateinit var periodRepository: CalendarPeriodRepository + + @Autowired + private lateinit var calendarRepository: CalendarRepository + + @Autowired + private lateinit var itemRepository: CalendarItemRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + private lateinit var testBranch: Branch + + @BeforeEach + fun setup() { + itemRepository.deleteAll() + calendarRepository.deleteAll() + periodRepository.deleteAll() + branchRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + } + + @Test + fun `saveCalendarPeriodDTO should create period and calendars for all branches with calendars`() { + // Create another branch + val branch2 = Branch().apply { + name = "Branch 2" + email = "branch2@example.com" + minimumAge = 9 + maximumAge = 11 + sex = Sex.UNKNOWN + description = "Test 2" + law = "Law" + image = "test2.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(branch2) + + // Create calendar for first branch to mark it as "has calendar" + val existingPeriod = CalendarPeriod().apply { + name = "Existing Period" + start = LocalDate.of(2023, 9, 1) + end = LocalDate.of(2024, 8, 31) + } + val savedPeriod = periodRepository.save(existingPeriod) + calendarRepository.save(Calendar(savedPeriod, testBranch)) + calendarRepository.flush() + + val dto = CalendarPeriodDTO( + id = null, + name = "2024-2025", + start = LocalDate.of(2024, 9, 1), + end = LocalDate.of(2025, 8, 31) + ) + + val saved = calendarService.saveCalendarPeriodDTO(dto) + + assertNotNull(saved.id) + assertEquals("2024-2025", saved.name) + + // Should create calendar for branch with existing calendar + val calendars = calendarRepository.getCalendarsByPeriod(periodRepository.findById(saved.id!!).get()) + assertEquals(1, calendars.size) + assertEquals(testBranch.id, calendars[0].branch.id) + } + + @Test + fun `saveCalendarPeriodDTO should reject overlapping periods`() { + val existingPeriod = CalendarPeriod().apply { + name = "Existing Period" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + periodRepository.save(existingPeriod) + + val overlappingDto = CalendarPeriodDTO( + id = null, + name = "Overlapping Period", + start = LocalDate.of(2025, 1, 1), + end = LocalDate.of(2025, 12, 31) + ) + + val exception = assertThrows(IllegalStateException::class.java) { + calendarService.saveCalendarPeriodDTO(overlappingDto) + } + + assertTrue(exception.message!!.contains("overlaps with existing periods")) + } + + @Test + fun `saveCalendarPeriodDTO should allow adjacent periods`() { + val existingPeriod = CalendarPeriod().apply { + name = "Existing Period" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + periodRepository.save(existingPeriod) + + val adjacentDto = CalendarPeriodDTO( + id = null, + name = "Adjacent Period", + start = LocalDate.of(2025, 9, 1), + end = LocalDate.of(2026, 8, 31) + ) + + val saved = calendarService.saveCalendarPeriodDTO(adjacentDto) + + assertNotNull(saved.id) + assertEquals("Adjacent Period", saved.name) + } + + @Test + fun `deleteCalendarPeriod should remove all associated calendars and items`() { + val period = CalendarPeriod().apply { + name = "To Delete" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + val savedPeriod = periodRepository.save(period) + + val calendar = Calendar(savedPeriod, testBranch) + val savedCalendar = calendarRepository.save(calendar) + + val item = CalendarItem().apply { + title = "Test Item" + content = "Content" + start = LocalDate.of(2024, 10, 6).atTime(14, 0) + end = LocalDate.of(2024, 10, 6).atTime(16, 0) + calendars = mutableListOf(savedCalendar) + } + itemRepository.save(item) + itemRepository.flush() + + calendarService.deleteCalendarPeriod(savedPeriod.id!!) + + assertFalse(periodRepository.existsById(savedPeriod.id!!)) + assertFalse(calendarRepository.existsById(savedCalendar.id!!)) + // Item should be deleted because it's not linked to any other calendar + assertFalse(itemRepository.existsById(item.id!!)) + } + + @Test + fun `deleteCalendarPeriod should keep items linked to other calendars`() { + val period1 = CalendarPeriod().apply { + name = "Period 1" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + val savedPeriod1 = periodRepository.save(period1) + + val period2 = CalendarPeriod().apply { + name = "Period 2" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + } + val savedPeriod2 = periodRepository.save(period2) + + val calendar1 = calendarRepository.save(Calendar(savedPeriod1, testBranch)) + val calendar2 = calendarRepository.save(Calendar(savedPeriod2, testBranch)) + + val sharedItem = CalendarItem().apply { + title = "Shared Item" + content = "Content" + start = LocalDate.of(2024, 10, 6).atTime(14, 0) + end = LocalDate.of(2024, 10, 6).atTime(16, 0) + calendars = mutableListOf(calendar1, calendar2) + } + val savedItem = itemRepository.save(sharedItem) + itemRepository.flush() + + calendarService.deleteCalendarPeriod(savedPeriod1.id!!) + + // Item should still exist, linked to calendar2 + assertTrue(itemRepository.existsById(savedItem.id!!)) + val remainingItem = itemRepository.findById(savedItem.id!!).get() + assertEquals(1, remainingItem.calendars.size) + assertEquals(calendar2.id, remainingItem.calendars[0].id) + } + + @Test + fun `getCurrentCalendars should return calendars in current period`() { + val currentPeriod = CalendarPeriod().apply { + name = "Current" + start = LocalDate.now().minusMonths(2) + end = LocalDate.now().plusMonths(10) + } + val savedPeriod = periodRepository.save(currentPeriod) + calendarRepository.save(Calendar(savedPeriod, testBranch)) + calendarRepository.flush() + + val calendars = calendarService.getCurrentCalendars() + + assertTrue(calendars.size >= 1) + assertTrue(calendars.any { it.period.name == "Current" }) + } + + @Test + fun `getCalendarDTOById with withDefaults should generate default Sunday items`() { + val period = CalendarPeriod().apply { + name = "Test Period" + start = LocalDate.of(2024, 10, 1) // Tuesday + end = LocalDate.of(2024, 10, 31) + } + val savedPeriod = periodRepository.save(period) + val calendar = calendarRepository.save(Calendar(savedPeriod, testBranch)) + calendarRepository.flush() + + val calendarDTO = calendarService.getCalendarDTOById(calendar.id!!, withDefaults = true) + + // October 2024 has Sundays on: 6, 13, 20, 27 + // Should generate default items for these dates + assertTrue(calendarDTO.items.size >= 4) + assertTrue(calendarDTO.items.any { it.start.toLocalDate().dayOfWeek == DayOfWeek.SUNDAY }) + } + + @Test + fun `getCalendarDTOById without withDefaults should not generate items`() { + val period = CalendarPeriod().apply { + name = "Test Period" + start = LocalDate.of(2024, 10, 1) + end = LocalDate.of(2024, 10, 31) + } + val savedPeriod = periodRepository.save(period) + val calendar = calendarRepository.save(Calendar(savedPeriod, testBranch)) + calendarRepository.flush() + + val calendarDTO = calendarService.getCalendarDTOById(calendar.id!!, withDefaults = false) + + assertEquals(0, calendarDTO.items.size) + } + + @Test + fun `getAllCalendarPeriods should return all periods`() { + val period1 = CalendarPeriod().apply { + name = "Period 1" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + val period2 = CalendarPeriod().apply { + name = "Period 2" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + } + periodRepository.save(period1) + periodRepository.save(period2) + periodRepository.flush() + + val periods = calendarService.getAllCalendarPeriods() + + assertTrue(periods.size >= 2) + assertTrue(periods.any { it.name == "Period 1" }) + assertTrue(periods.any { it.name == "Period 2" }) + } + + @Test + fun `mergeCalendarPeriodDTOChanges should update period dates`() { + val period = CalendarPeriod().apply { + name = "Original" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + val saved = periodRepository.save(period) + periodRepository.flush() + + val dto = CalendarPeriodDTO( + id = saved.id, + name = "Updated", + start = LocalDate.of(2024, 10, 1), + end = LocalDate.of(2025, 9, 30) + ) + + val updated = calendarService.mergeCalendarPeriodDTOChanges(saved.id!!, dto) + + assertEquals("Updated", updated.name) + assertEquals(LocalDate.of(2024, 10, 1), updated.start) + assertEquals(LocalDate.of(2025, 9, 30), updated.end) + } + + @Test + fun `mergeCalendarPeriodDTOChanges should reject overlaps with other periods`() { + val period1 = CalendarPeriod().apply { + name = "Period 1" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + } + val saved1 = periodRepository.save(period1) + + val period2 = CalendarPeriod().apply { + name = "Period 2" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + } + val saved2 = periodRepository.save(period2) + periodRepository.flush() + + val dto = CalendarPeriodDTO( + id = saved2.id, + name = "Period 2", + start = LocalDate.of(2025, 1, 1), // Overlaps with period1 + end = LocalDate.of(2026, 8, 31) + ) + + val exception = assertThrows(IllegalStateException::class.java) { + calendarService.mergeCalendarPeriodDTOChanges(saved2.id!!, dto) + } + + assertTrue(exception.message!!.contains("overlaps")) + } + + @Test + fun `getCalendarPeriodDTOById should throw exception when not found`() { + assertThrows(CalendarPeriodNotFoundException::class.java) { + calendarService.getCalendarPeriodDTOById(999) + } + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/NewsItemServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/NewsItemServiceIntegrationTest.kt new file mode 100644 index 0000000..03c53dc --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/NewsItemServiceIntegrationTest.kt @@ -0,0 +1,128 @@ +package be.sgl.backend.service + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.NewsItem +import be.sgl.backend.repository.NewsItemRepository +import be.sgl.backend.service.exception.NewsItemNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class NewsItemServiceIntegrationTest { + + @Autowired + private lateinit var newsItemService: NewsItemService + + @Autowired + private lateinit var newsItemRepository: NewsItemRepository + + @BeforeEach + fun setup() { + newsItemRepository.deleteAll() + } + + @Test + fun `getVisibleItems should only return visible news items`() { + val visibleItem = NewsItem().apply { + title = "Visible News" + content = "This is visible" + visible = true + } + + val hiddenItem = NewsItem().apply { + title = "Hidden News" + content = "This is hidden" + visible = false + } + + newsItemRepository.save(visibleItem) + newsItemRepository.save(hiddenItem) + newsItemRepository.flush() + + val items = newsItemService.getVisibleItems() + + assertTrue(items.any { it.title == "Visible News" }) + assertFalse(items.any { it.title == "Hidden News" }) + } + + @Test + fun `getNewsItemDTOById should return item when exists`() { + val newsItem = NewsItem().apply { + title = "Test News" + content = "Test content" + visible = true + } + val saved = newsItemRepository.save(newsItem) + newsItemRepository.flush() + + val result = newsItemService.getNewsItemDTOById(saved.id!!) + + assertNotNull(result) + assertEquals("Test News", result.title) + assertEquals("Test content", result.content) + } + + @Test + fun `getNewsItemDTOById should throw exception when not found`() { + assertThrows(NewsItemNotFoundException::class.java) { + newsItemService.getNewsItemDTOById(999) + } + } + + @Test + fun `deleteNewsItem should remove news item`() { + val newsItem = NewsItem().apply { + title = "To Delete" + content = "Will be deleted" + visible = true + } + val saved = newsItemRepository.save(newsItem) + newsItemRepository.flush() + + newsItemService.deleteNewsItem(saved.id!!) + + assertFalse(newsItemRepository.existsById(saved.id!!)) + } + + @Test + fun `deleteNewsItem should throw exception when not found`() { + assertThrows(NewsItemNotFoundException::class.java) { + newsItemService.deleteNewsItem(999) + } + } + + @Test + fun `news items should be ordered by creation date`() { + val older = NewsItem().apply { + title = "Older News" + content = "Older content" + visible = true + } + + val newer = NewsItem().apply { + title = "Newer News" + content = "Newer content" + visible = true + } + + newsItemRepository.save(older) + Thread.sleep(10) // Ensure different timestamps + newsItemRepository.save(newer) + newsItemRepository.flush() + + val items = newsItemService.getVisibleItems() + + assertTrue(items.size >= 2) + // Newer items should come first + val newerIndex = items.indexOfFirst { it.title == "Newer News" } + val olderIndex = items.indexOfFirst { it.title == "Older News" } + assertTrue(newerIndex < olderIndex) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/RoleServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/RoleServiceIntegrationTest.kt new file mode 100644 index 0000000..823d9c8 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/RoleServiceIntegrationTest.kt @@ -0,0 +1,284 @@ +package be.sgl.backend.service + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.RoleDTO +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.user.Role +import be.sgl.backend.entity.user.RoleLevel +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.RoleRepository +import be.sgl.backend.service.exception.BranchNotFoundException +import be.sgl.backend.service.exception.RoleNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class RoleServiceIntegrationTest { + + @Autowired + private lateinit var roleService: RoleService + + @Autowired + private lateinit var roleRepository: RoleRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + private lateinit var testBranch: Branch + + @BeforeEach + fun setup() { + roleRepository.deleteAll() + branchRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + } + + @Test + fun `getAllRoles should return all roles from database`() { + val role1 = Role().apply { + name = "Leader" + level = RoleLevel.STAFF + } + val role2 = Role().apply { + name = "Assistant" + level = RoleLevel.STAFF + } + + roleRepository.save(role1) + roleRepository.save(role2) + roleRepository.flush() + + val roles = roleService.getAllRoles() + + assertTrue(roles.size >= 2) + assertTrue(roles.any { it.name == "Leader" }) + assertTrue(roles.any { it.name == "Assistant" }) + } + + @Test + fun `saveRoleDTO should persist role to database`() { + val dto = RoleDTO( + id = null, + name = "New Role", + externalId = null, + backupExternalId = null, + branch = null, + staffBranch = null, + level = RoleLevel.STAFF + ) + + val saved = roleService.saveRoleDTO(dto) + + assertNotNull(saved.id) + assertEquals("New Role", saved.name) + assertEquals(RoleLevel.STAFF, saved.level) + + // Verify it's in the database + val fromDb = roleRepository.findById(saved.id!!).get() + assertEquals("New Role", fromDb.name) + } + + @Test + fun `mergeRoleDTOChanges should update existing role`() { + val role = Role().apply { + name = "Original Role" + level = RoleLevel.STAFF + } + val savedRole = roleRepository.save(role) + roleRepository.flush() + + val dto = RoleDTO( + id = savedRole.id, + name = "Updated Role", + externalId = "ext-123", + backupExternalId = "backup-456", + branch = null, + staffBranch = null, + level = RoleLevel.ADMIN + ) + + val updated = roleService.mergeRoleDTOChanges(savedRole.id!!, dto) + + assertEquals("Updated Role", updated.name) + assertEquals("ext-123", updated.externalId) + assertEquals("backup-456", updated.backupExternalId) + assertEquals(RoleLevel.ADMIN, updated.level) + + // Verify changes are persisted + val fromDb = roleRepository.findById(savedRole.id!!).get() + assertEquals("Updated Role", fromDb.name) + assertEquals(RoleLevel.ADMIN, fromDb.level) + } + + @Test + fun `mergeRoleDTOChanges should update branch associations`() { + val role = Role().apply { + name = "Role with Branch" + level = RoleLevel.STAFF + } + val savedRole = roleRepository.save(role) + roleRepository.flush() + + val dto = RoleDTO( + id = savedRole.id, + name = "Role with Branch", + externalId = null, + backupExternalId = null, + branch = be.sgl.backend.dto.BranchBaseDTO( + id = testBranch.id, + name = testBranch.name, + description = testBranch.description, + image = testBranch.image, + minimumAge = testBranch.minimumAge, + maximumAge = testBranch.maximumAge, + sex = testBranch.sex + ), + staffBranch = null, + level = RoleLevel.STAFF + ) + + val updated = roleService.mergeRoleDTOChanges(savedRole.id!!, dto) + + assertNotNull(updated.branch) + assertEquals(testBranch.id, updated.branch?.id) + } + + @Test + fun `deleteRole should remove role from database`() { + val role = Role().apply { + name = "To Delete" + level = RoleLevel.STAFF + } + val savedRole = roleRepository.save(role) + roleRepository.flush() + + roleService.deleteRole(savedRole.id!!) + + assertFalse(roleRepository.existsById(savedRole.id!!)) + } + + @Test + fun `deleteRole should throw exception when role not found`() { + assertThrows(RoleNotFoundException::class.java) { + roleService.deleteRole(999) + } + } + + @Test + fun `mergeRoleDTOChanges should throw exception when role not found`() { + val dto = RoleDTO( + id = 999, + name = "Non-existent", + externalId = null, + backupExternalId = null, + branch = null, + staffBranch = null, + level = RoleLevel.STAFF + ) + + assertThrows(RoleNotFoundException::class.java) { + roleService.mergeRoleDTOChanges(999, dto) + } + } + + @Test + fun `mergeRoleDTOChanges should throw exception when branch not found`() { + val role = Role().apply { + name = "Role" + level = RoleLevel.STAFF + } + val savedRole = roleRepository.save(role) + roleRepository.flush() + + val dto = RoleDTO( + id = savedRole.id, + name = "Role", + externalId = null, + backupExternalId = null, + branch = be.sgl.backend.dto.BranchBaseDTO( + id = 999, // Non-existent branch + name = "Fake", + description = "Fake", + image = "fake.jpg", + minimumAge = 6, + maximumAge = 8, + sex = Sex.UNKNOWN + ), + staffBranch = null, + level = RoleLevel.STAFF + ) + + assertThrows(BranchNotFoundException::class.java) { + roleService.mergeRoleDTOChanges(savedRole.id!!, dto) + } + } + + @Test + fun `saveRoleDTO should handle all role levels`() { + val levels = listOf( + RoleLevel.GUEST, + RoleLevel.STAFF, + RoleLevel.ADMIN + ) + + levels.forEach { level -> + val dto = RoleDTO( + id = null, + name = "Role for $level", + externalId = null, + backupExternalId = null, + branch = null, + staffBranch = null, + level = level + ) + + val saved = roleService.saveRoleDTO(dto) + assertEquals(level, saved.level) + } + } + + @Test + fun `roles should persist with external IDs`() { + val dto = RoleDTO( + id = null, + name = "External Role", + externalId = "ext-abc-123", + backupExternalId = "backup-xyz-789", + branch = null, + staffBranch = null, + level = RoleLevel.STAFF + ) + + val saved = roleService.saveRoleDTO(dto) + + assertEquals("ext-abc-123", saved.externalId) + assertEquals("backup-xyz-789", saved.backupExternalId) + + // Verify persistence + val fromDb = roleRepository.findById(saved.id!!).get() + assertEquals("ext-abc-123", fromDb.externalId) + assertEquals("backup-xyz-789", fromDb.backupExternalId) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/SettingServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/SettingServiceIntegrationTest.kt new file mode 100644 index 0000000..6309e61 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/SettingServiceIntegrationTest.kt @@ -0,0 +1,161 @@ +package be.sgl.backend.service + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.setting.Setting +import be.sgl.backend.entity.setting.SettingId +import be.sgl.backend.repository.SettingRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class SettingServiceIntegrationTest { + + @Autowired + private lateinit var settingService: SettingService + + @Autowired + private lateinit var settingRepository: SettingRepository + + @BeforeEach + fun setup() { + settingRepository.deleteAll() + } + + @Test + fun `get should return value from database`() { + val setting = Setting(SettingId.CALENDAR_NAME, "Integration Test Calendar") + settingRepository.save(setting) + settingRepository.flush() + + val result = settingService.get(SettingId.CALENDAR_NAME) + + assertEquals("Integration Test Calendar", result) + } + + @Test + fun `get should return null when setting does not exist in database`() { + val result = settingService.get(SettingId.CALENDAR_NAME) + + assertNull(result) + } + + @Test + fun `getOrDefault with String should return value from database`() { + val setting = Setting(SettingId.CALENDAR_NAME, "Database Calendar") + settingRepository.save(setting) + settingRepository.flush() + + val result = settingService.getOrDefault(SettingId.CALENDAR_NAME, "Default Calendar") + + assertEquals("Database Calendar", result) + } + + @Test + fun `getOrDefault with String should return default when not in database`() { + val result = settingService.getOrDefault(SettingId.CALENDAR_NAME, "Default Calendar") + + assertEquals("Default Calendar", result) + } + + @Test + fun `getOrDefault with Double should parse value from database`() { + val setting = Setting(SettingId.LATEST_DISPATCH_RATE, "42.75") + settingRepository.save(setting) + settingRepository.flush() + + val result = settingService.getOrDefault(SettingId.LATEST_DISPATCH_RATE, 0.0) + + assertEquals(42.75, result) + } + + @Test + fun `getOrDefault with Double should return default when not in database`() { + val result = settingService.getOrDefault(SettingId.LATEST_DISPATCH_RATE, 10.0) + + assertEquals(10.0, result) + } + + @Test + fun `update should persist new setting to database`() { + settingService.update(SettingId.CALENDAR_NAME, "Persisted Calendar") + + val saved = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assertNotNull(saved) + assertEquals("Persisted Calendar", saved?.value) + } + + @Test + fun `update should modify existing setting in database`() { + val setting = Setting(SettingId.CALENDAR_NAME, "Original") + settingRepository.save(setting) + settingRepository.flush() + + settingService.update(SettingId.CALENDAR_NAME, "Modified") + + val updated = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assertEquals("Modified", updated?.value) + } + + @Test + fun `update with null should delete setting from database`() { + val setting = Setting(SettingId.CALENDAR_NAME, "To Delete") + settingRepository.save(setting) + settingRepository.flush() + + settingService.update(SettingId.CALENDAR_NAME, null) + + val deleted = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assertNull(deleted) + } + + @Test + fun `update should handle numeric values and persist correctly`() { + settingService.update(SettingId.LATEST_DISPATCH_RATE, 99.99) + + val saved = settingRepository.findByIdOrNull(SettingId.LATEST_DISPATCH_RATE.name) + assertEquals("99.99", saved?.value) + + // Should be able to retrieve as double + val retrieved = settingService.getOrDefault(SettingId.LATEST_DISPATCH_RATE, 0.0) + assertEquals(99.99, retrieved) + } + + @Test + fun `update should handle boolean values and persist correctly`() { + settingService.update(SettingId.CALENDAR_NAME, true) + + val saved = settingRepository.findByIdOrNull(SettingId.CALENDAR_NAME.name) + assertEquals("true", saved?.value) + } + + @Test + fun `multiple updates should persist correctly`() { + settingService.update(SettingId.CALENDAR_NAME, "First") + settingService.update(SettingId.ORGANIZATION_NAME, "Org Name") + settingService.update(SettingId.REPRESENTATIVE_TITLE, "Title") + + assertEquals("First", settingService.get(SettingId.CALENDAR_NAME)) + assertEquals("Org Name", settingService.get(SettingId.ORGANIZATION_NAME)) + assertEquals("Title", settingService.get(SettingId.REPRESENTATIVE_TITLE)) + } + + @Test + fun `settings should persist across service calls`() { + settingService.update(SettingId.CALENDAR_NAME, "Persistent Value") + settingRepository.flush() + + // Simulate new service usage + val retrieved1 = settingService.get(SettingId.CALENDAR_NAME) + val retrieved2 = settingService.get(SettingId.CALENDAR_NAME) + + assertEquals("Persistent Value", retrieved1) + assertEquals("Persistent Value", retrieved2) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/SettingServiceTest.kt b/src/test/kotlin/be/sgl/backend/service/SettingServiceTest.kt new file mode 100644 index 0000000..584a5b2 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/SettingServiceTest.kt @@ -0,0 +1,153 @@ +package be.sgl.backend.service + +import be.sgl.backend.entity.setting.Setting +import be.sgl.backend.entity.setting.SettingId +import be.sgl.backend.repository.SettingRepository +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations + +class SettingServiceTest { + + @Mock + private lateinit var settingRepository: SettingRepository + + @InjectMocks + private lateinit var settingService: SettingService + + @BeforeEach + fun setup() { + MockitoAnnotations.openMocks(this) + } + + @Test + fun `get should return value when setting exists`() { + val settingId = SettingId.ORGANIZATION_NAME + val setting = Setting(settingId, "Test Organization") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(setting) + + val result = settingService.get(settingId) + + assertEquals("Test Organization", result) + verify(settingRepository).findByIdOrNull(settingId.name) + } + + @Test + fun `get should return null when setting does not exist`() { + val settingId = SettingId.ORGANIZATION_NAME + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(null) + + val result = settingService.get(settingId) + + assertNull(result) + verify(settingRepository).findByIdOrNull(settingId.name) + } + + @Test + fun `getOrDefault with String default should return value when setting exists`() { + val settingId = SettingId.ORGANIZATION_NAME + val setting = Setting(settingId, "Test Organization") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(setting) + + val result = settingService.getOrDefault(settingId, "Default Name") + + assertEquals("Test Organization", result) + } + + @Test + fun `getOrDefault with String default should return default when setting does not exist`() { + val settingId = SettingId.ORGANIZATION_NAME + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(null) + + val result = settingService.getOrDefault(settingId, "Default Name") + + assertEquals("Default Name", result) + } + + @Test + fun `getOrDefault with Double default should return value when setting exists`() { + val settingId = SettingId.ORGANIZATION_NAME + val setting = Setting(settingId, "42.5") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(setting) + + val result = settingService.getOrDefault(settingId, 0.0) + + assertEquals(42.5, result) + } + + @Test + fun `getOrDefault with Double default should return default when setting does not exist`() { + val settingId = SettingId.ORGANIZATION_NAME + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(null) + + val result = settingService.getOrDefault(settingId, 10.0) + + assertEquals(10.0, result) + } + + @Test + fun `update should create new setting when it does not exist`() { + val settingId = SettingId.ORGANIZATION_NAME + val newSetting = Setting(settingId, "New Organization") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(null) + `when`(settingRepository.save(any(Setting::class.java))).thenReturn(newSetting) + + settingService.update(settingId, "New Organization") + + verify(settingRepository).findByIdOrNull(settingId.name) + verify(settingRepository).save(any(Setting::class.java)) + } + + @Test + fun `update should update existing setting when it exists`() { + val settingId = SettingId.ORGANIZATION_NAME + val existingSetting = Setting(settingId, "Old Organization") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(existingSetting) + `when`(settingRepository.save(existingSetting)).thenReturn(existingSetting) + + settingService.update(settingId, "Updated Organization") + + assertEquals("Updated Organization", existingSetting.value) + verify(settingRepository).save(existingSetting) + } + + @Test + fun `update should delete setting when value is null`() { + val settingId = SettingId.ORGANIZATION_NAME + + settingService.update(settingId, null) + + verify(settingRepository).deleteById(settingId.name) + verify(settingRepository, never()).save(any(Setting::class.java)) + } + + @Test + fun `update should handle numeric values`() { + val settingId = SettingId.ORGANIZATION_NAME + val existingSetting = Setting(settingId, "0") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(existingSetting) + `when`(settingRepository.save(existingSetting)).thenReturn(existingSetting) + + settingService.update(settingId, 42) + + assertEquals("42", existingSetting.value) + verify(settingRepository).save(existingSetting) + } + + @Test + fun `update should handle boolean values`() { + val settingId = SettingId.ORGANIZATION_NAME + val existingSetting = Setting(settingId, "false") + `when`(settingRepository.findByIdOrNull(settingId.name)).thenReturn(existingSetting) + `when`(settingRepository.save(existingSetting)).thenReturn(existingSetting) + + settingService.update(settingId, true) + + assertEquals("true", existingSetting.value) + verify(settingRepository).save(existingSetting) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/activity/ActivityRegistrationServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/activity/ActivityRegistrationServiceIntegrationTest.kt new file mode 100644 index 0000000..58c62a5 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/activity/ActivityRegistrationServiceIntegrationTest.kt @@ -0,0 +1,533 @@ +package be.sgl.backend.service.activity + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.membership.Membership +import be.sgl.backend.entity.membership.MembershipPeriod +import be.sgl.backend.entity.registrable.activity.Activity +import be.sgl.backend.entity.registrable.activity.ActivityRegistration +import be.sgl.backend.entity.registrable.activity.ActivityRestriction +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.Sibling +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.activity.ActivityRegistrationRepository +import be.sgl.backend.repository.activity.ActivityRepository +import be.sgl.backend.repository.activity.ActivityRestrictionRepository +import be.sgl.backend.repository.membership.MembershipPeriodRepository +import be.sgl.backend.repository.membership.MembershipRepository +import be.sgl.backend.repository.user.SiblingRepository +import be.sgl.backend.repository.user.UserRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate +import java.time.LocalDateTime + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class ActivityRegistrationServiceIntegrationTest { + + @Autowired + private lateinit var registrationService: ActivityRegistrationService + + @Autowired + private lateinit var activityRepository: ActivityRepository + + @Autowired + private lateinit var registrationRepository: ActivityRegistrationRepository + + @Autowired + private lateinit var restrictionRepository: ActivityRestrictionRepository + + @Autowired + private lateinit var membershipRepository: MembershipRepository + + @Autowired + private lateinit var membershipPeriodRepository: MembershipPeriodRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var siblingRepository: SiblingRepository + + private lateinit var testUser: User + private lateinit var testBranch: Branch + private lateinit var testActivity: Activity + private lateinit var testRestriction: ActivityRestriction + private lateinit var testPeriod: MembershipPeriod + + @BeforeEach + fun setup() { + siblingRepository.deleteAll() + registrationRepository.deleteAll() + restrictionRepository.deleteAll() + activityRepository.deleteAll() + membershipRepository.deleteAll() + userRepository.deleteAll() + branchRepository.deleteAll() + membershipPeriodRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + + testUser = User().apply { + username = "testuser" + firstName = "Test" + name = "User" + email = "testuser@example.com" + birthdate = LocalDate.of(2014, 1, 1) // Age 10-11 + sex = Sex.UNKNOWN + hasReduction = false + hasHandicap = false + } + testUser = userRepository.save(testUser) + + testPeriod = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.now().minusMonths(2) + end = LocalDate.now().plusMonths(10) + basePrice = 100.0 + } + testPeriod = membershipPeriodRepository.save(testPeriod) + + // Create active membership for user + val membership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership) + + testActivity = Activity().apply { + name = "Test Activity" + description = "Test Description" + open = LocalDateTime.now().minusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + reductionFactor = 3 + siblingReduction = 10.0 + cancellable = true + sendConfirmation = true + sendCompleteConfirmation = false + } + testActivity = activityRepository.save(testActivity) + + testRestriction = ActivityRestriction().apply { + name = "Test Restriction" + activity = testActivity + branch = testBranch + } + testRestriction = restrictionRepository.save(testRestriction) + } + + @Test + fun `getStatusForActivityAndUser should return open options when user has active membership`() { + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertTrue(status.activeMembership) + assertEquals(1, status.openOptions.size) + assertEquals(testRestriction.id, status.openOptions[0].id) + assertTrue(status.closedOptions.isEmpty()) + assertNull(status.currentRegistration) + } + + @Test + fun `getStatusForActivityAndUser should return no active membership when user has no membership`() { + membershipRepository.deleteAll() + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertFalse(status.activeMembership) + assertTrue(status.openOptions.isEmpty()) + } + + @Test + fun `getStatusForActivityAndUser should apply reduction for user with hasReduction flag`() { + testUser.hasReduction = true + userRepository.save(testUser) + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertTrue(status.openOptions.isNotEmpty()) + // Price should be divided by reductionFactor (3) + val expectedPrice = 50.0 / 3 + assertEquals(expectedPrice, status.openOptions[0].alternativePrice) + } + + @Test + fun `getStatusForActivityAndUser should show closed options when global limit is reached`() { + testActivity.registrationLimit = 1 + activityRepository.save(testActivity) + + // Create another user and registration + val otherUser = User().apply { + username = "otheruser" + firstName = "Other" + name = "User" + email = "other@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(otherUser) + + val registration = ActivityRegistration(testActivity, otherUser, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(registration) + registrationRepository.flush() + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertTrue(status.openOptions.isEmpty()) + assertEquals(1, status.closedOptions.size) + } + + @Test + fun `getStatusForActivityAndUser should show closed options when restriction limit is reached`() { + testRestriction.alternativeLimit = 1 + restrictionRepository.save(testRestriction) + + val otherUser = User().apply { + username = "otheruser" + firstName = "Other" + name = "User" + email = "other@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(otherUser) + + val registration = ActivityRegistration(testActivity, otherUser, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(registration) + registrationRepository.flush() + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertTrue(status.openOptions.isEmpty()) + assertEquals(1, status.closedOptions.size) + } + + @Test + fun `getStatusForActivityAndUser should show closed options when branch limit is reached`() { + testActivity.branchLimits = mutableMapOf(testBranch.id!! to 1) + activityRepository.save(testActivity) + + val otherUser = User().apply { + username = "otheruser" + firstName = "Other" + name = "User" + email = "other@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(otherUser) + + // Create membership for other user in same branch + val membership = Membership(testPeriod, otherUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership) + + val registration = ActivityRegistration(testActivity, otherUser, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(registration) + registrationRepository.flush() + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertTrue(status.openOptions.isEmpty()) + assertEquals(1, status.closedOptions.size) + } + + @Test + fun `getStatusForActivityAndUser should return current registration when already registered`() { + val registration = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(registration) + registrationRepository.flush() + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertNotNull(status.currentRegistration) + assertEquals(registration.id, status.currentRegistration!!.id) + } + + @Test + fun `getAllRegistrationsForActivity should return only paid registrations`() { + val paidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(paidReg) + + val otherUser = User().apply { + username = "unpaiduser" + firstName = "Unpaid" + name = "User" + email = "unpaid@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(otherUser) + + val unpaidReg = ActivityRegistration(testActivity, otherUser, testRestriction, 50.0, null).apply { + paid = false + } + registrationRepository.save(unpaidReg) + registrationRepository.flush() + + val registrations = registrationService.getAllRegistrationsForActivity(testActivity.id!!) + + assertEquals(1, registrations.size) + assertEquals(testUser.email, registrations[0].user.email) + } + + @Test + fun `getAllRegistrationsForUser should return all registrations for user`() { + val reg1 = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(reg1) + + val activity2 = Activity().apply { + name = "Activity 2" + description = "Test" + open = LocalDateTime.now().minusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 75.0 + } + activityRepository.save(activity2) + + val restriction2 = ActivityRestriction().apply { + name = "Restriction 2" + activity = activity2 + branch = testBranch + } + restrictionRepository.save(restriction2) + + val reg2 = ActivityRegistration(activity2, testUser, restriction2, 75.0, null).apply { + paid = true + } + registrationRepository.save(reg2) + registrationRepository.flush() + + val registrations = registrationService.getAllRegistrationsForUser(testUser.username!!) + + assertEquals(2, registrations.size) + } + + @Test + fun `markRegistrationAsCompleted should fail for unpaid registration`() { + val unpaidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = false + } + val saved = registrationRepository.save(unpaidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.markRegistrationAsCompleted(saved.id!!) + } + + assertEquals("Only a paid activity can be marked as completed!", exception.message) + } + + @Test + fun `markRegistrationAsCompleted should fail if activity hasn't started yet`() { + testActivity.start = LocalDateTime.now().plusDays(5) + activityRepository.save(testActivity) + + val paidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.markRegistrationAsCompleted(saved.id!!) + } + + assertEquals("Registrations can only be completed starting one hour before the activity!", exception.message) + } + + @Test + fun `markRegistrationAsCompleted should succeed for paid registration near activity start`() { + testActivity.start = LocalDateTime.now().minusMinutes(30) + testActivity.end = LocalDateTime.now().plusHours(2) + activityRepository.save(testActivity) + + val paidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + completed = false + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + registrationService.markRegistrationAsCompleted(saved.id!!) + + val updated = registrationRepository.findById(saved.id!!).get() + assertTrue(updated.completed) + } + + @Test + fun `markRegistrationAsCompleted should not fail if already completed`() { + testActivity.start = LocalDateTime.now().minusMinutes(30) + testActivity.end = LocalDateTime.now().plusHours(2) + activityRepository.save(testActivity) + + val paidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + completed = true + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + assertDoesNotThrow { + registrationService.markRegistrationAsCompleted(saved.id!!) + } + + val registration = registrationRepository.findById(saved.id!!).get() + assertTrue(registration.completed) + } + + @Test + fun `cancelRegistration should fail for unpaid registration`() { + val unpaidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = false + } + val saved = registrationRepository.save(unpaidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.cancelRegistration(saved.id!!) + } + + assertEquals("Only a paid activity registration can be cancelled!", exception.message) + } + + @Test + fun `cancelRegistration should fail when registrations are closed`() { + testActivity.closed = LocalDateTime.now().minusDays(1) + activityRepository.save(testActivity) + + val paidReg = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.cancelRegistration(saved.id!!) + } + + assertEquals("Cancellation is only possible when registrations are still open!", exception.message) + } + + @Test + fun `price calculation should apply sibling reduction when sibling already registered`() { + // Create sibling user + val sibling = User().apply { + username = "sibling" + firstName = "Sibling" + name = "User" + email = "sibling@example.com" + birthdate = LocalDate.of(2012, 1, 1) + hasReduction = false + } + userRepository.save(sibling) + + // Create sibling relationship + val siblingRelation = Sibling(testUser, sibling) + siblingRepository.save(siblingRelation) + + // Register sibling for activity + val siblingRegistration = ActivityRegistration(testActivity, sibling, testRestriction, 50.0, null).apply { + paid = true + } + registrationRepository.save(siblingRegistration) + registrationRepository.flush() + + // Get status for test user - should show reduced price + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + // Price should be reduced by siblingReduction (10.0) -> 50.0 - 10.0 = 40.0 + // However, status shows alternativePrice which is the base price + // The reduction is applied during payment creation + assertTrue(status.openOptions.isNotEmpty()) + } + + @Test + fun `getActivityRegistrationDTOById should return registration when exists`() { + val registration = ActivityRegistration(testActivity, testUser, testRestriction, 50.0, null).apply { + paid = true + } + val saved = registrationRepository.save(registration) + registrationRepository.flush() + + val result = registrationService.getActivityRegistrationDTOById(saved.id!!) + + assertNotNull(result) + assertEquals(testUser.email, result?.user?.email) + } + + @Test + fun `getActivityRegistrationDTOById should return null when not found`() { + val result = registrationService.getActivityRegistrationDTOById(999) + + assertNull(result) + } + + @Test + fun `price calculation should handle additional data with JSONata`() { + testActivity.additionalFormRule = "nights * 15" + activityRepository.save(testActivity) + + val registration = ActivityRegistration( + testActivity, + testUser, + testRestriction, + 50.0, + "{\"nights\":\"2\"}" + ).apply { + paid = true + } + registrationRepository.save(registration) + registrationRepository.flush() + + // Additional price should be 2 * 15 = 30.0, total with base price would be 80.0 + val result = registrationService.getActivityRegistrationDTOById(registration.id!!) + assertNotNull(result) + } + + @Test + fun `restriction with alternative price should use alternative price instead of base price`() { + testRestriction.alternativePrice = 25.0 + restrictionRepository.save(testRestriction) + + val status = registrationService.getStatusForActivityAndUser(testActivity.id!!, testUser.username!!) + + assertEquals(25.0, status.openOptions[0].alternativePrice) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/activity/ActivityServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/activity/ActivityServiceIntegrationTest.kt new file mode 100644 index 0000000..0fbc704 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/activity/ActivityServiceIntegrationTest.kt @@ -0,0 +1,347 @@ +package be.sgl.backend.service.activity + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.ActivityDTO +import be.sgl.backend.entity.Address +import be.sgl.backend.entity.registrable.activity.Activity +import be.sgl.backend.entity.registrable.activity.ActivityRegistration +import be.sgl.backend.entity.registrable.activity.ActivityRestriction +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.activity.ActivityRegistrationRepository +import be.sgl.backend.repository.activity.ActivityRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDateTime + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class ActivityServiceIntegrationTest { + + @Autowired + private lateinit var activityService: ActivityService + + @Autowired + private lateinit var activityRepository: ActivityRepository + + @Autowired + private lateinit var registrationRepository: ActivityRegistrationRepository + + private lateinit var testActivity: Activity + private lateinit var testUser: User + + @BeforeEach + fun setup() { + testUser = User().apply { + name = "Test" + firstName = "User" + email = "test@example.com" + birthdate = java.time.LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + + testActivity = Activity().apply { + name = "Test Activity" + description = "Test Description" + open = LocalDateTime.now().plusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + registrationLimit = 10 + cancellable = true + sendConfirmation = true + sendCompleteConfirmation = false + } + } + + @Test + fun `getAllActivities should return all activities with registration data`() { + val savedActivity = activityRepository.save(testActivity) + activityRepository.flush() + + val activities = activityService.getAllActivities() + + assertNotNull(activities) + assertTrue(activities.any { it.activity.id == savedActivity.id }) + } + + @Test + fun `getVisibleActivities should only return non-cancelled activities`() { + val visibleActivity = testActivity + val cancelledActivity = Activity().apply { + name = "Cancelled Activity" + description = "Cancelled" + open = LocalDateTime.now().plusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + cancelled = true + } + + activityRepository.save(visibleActivity) + activityRepository.save(cancelledActivity) + activityRepository.flush() + + val activities = activityService.getVisibleActivities() + + assertTrue(activities.any { it.name == "Test Activity" }) + assertFalse(activities.any { it.name == "Cancelled Activity" }) + } + + @Test + fun `saveActivityDTO should validate open-closed-start-end sequence`() { + val dto = ActivityDTO( + id = null, + name = "Invalid Activity", + description = "Test", + open = LocalDateTime.now().plusDays(10), + closed = LocalDateTime.now().plusDays(5), // Closed before open! + start = LocalDateTime.now().plusDays(15), + end = LocalDateTime.now().plusDays(20), + price = 50.0, + reductionFactor = 3.0, + siblingReduction = 10.0, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null, + restrictions = emptyList() + ) + + val exception = assertThrows(IllegalStateException::class.java) { + activityService.saveActivityDTO(dto) + } + + assertEquals("The closure of registrations should be after the opening of registrations!", exception.message) + } + + @Test + fun `saveActivityDTO should validate closed before start`() { + val dto = ActivityDTO( + id = null, + name = "Invalid Activity", + description = "Test", + open = LocalDateTime.now().plusDays(5), + closed = LocalDateTime.now().plusDays(10), + start = LocalDateTime.now().plusDays(8), // Start before closed! + end = LocalDateTime.now().plusDays(20), + price = 50.0, + reductionFactor = 3.0, + siblingReduction = 10.0, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null, + restrictions = emptyList() + ) + + val exception = assertThrows(IllegalStateException::class.java) { + activityService.saveActivityDTO(dto) + } + + assertEquals("The start date of an activity should be after the closure of registrations!", exception.message) + } + + @Test + fun `mergeActivityDTOChanges should not allow editing cancelled activity`() { + testActivity.cancelled = true + val savedActivity = activityRepository.save(testActivity) + activityRepository.flush() + + val dto = ActivityDTO( + id = savedActivity.id, + name = "Updated Activity", + description = "Updated", + open = testActivity.open, + closed = testActivity.closed, + start = testActivity.start, + end = testActivity.end, + price = 60.0, + reductionFactor = 3.0, + siblingReduction = 10.0, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null, + restrictions = emptyList() + ) + + val exception = assertThrows(IllegalStateException::class.java) { + activityService.mergeActivityDTOChanges(savedActivity.id!!, dto) + } + + assertEquals("A cancelled activity cannot be edited anymore!", exception.message) + } + + @Test + fun `mergeActivityDTOChanges should allow full edit when not yet open`() { + val savedActivity = activityRepository.save(testActivity) + activityRepository.flush() + + val dto = ActivityDTO( + id = savedActivity.id, + name = "Updated Activity", + description = "Updated Description", + open = testActivity.open, + closed = testActivity.closed, + start = testActivity.start, + end = testActivity.end, + price = 75.0, + reductionFactor = 2.0, + siblingReduction = 15.0, + registrationLimit = 20, + address = null, + additionalForm = "{\"field\": \"value\"}", + additionalFormRule = "price + 10", + cancellable = false, + sendConfirmation = false, + sendCompleteConfirmation = true, + communicationCC = "cc@example.com", + restrictions = emptyList() + ) + + val updatedActivity = activityService.mergeActivityDTOChanges(savedActivity.id!!, dto) + + assertEquals("Updated Activity", updatedActivity.name) + assertEquals(75.0, updatedActivity.price) + assertEquals(2.0, updatedActivity.reductionFactor) + assertEquals(15.0, updatedActivity.siblingReduction) + } + + @Test + fun `mergeActivityDTOChanges should not allow lowering registration limit below current registrations`() { + // Create activity with limit + testActivity.registrationLimit = 10 + testActivity.open = LocalDateTime.now().minusDays(5) // Already open + testActivity.closed = LocalDateTime.now().plusDays(5) + val savedActivity = activityRepository.save(testActivity) + activityRepository.flush() + + // Create 3 paid registrations + repeat(3) { index -> + val user = User().apply { + name = "User$index" + firstName = "Test" + email = "user$index@example.com" + birthdate = java.time.LocalDate.of(2010, 1, 1) + sex = Sex.MALE + } + val registration = ActivityRegistration(user, savedActivity, savedActivity.price).apply { + paid = true + } + registrationRepository.save(registration) + } + registrationRepository.flush() + + val dto = ActivityDTO( + id = savedActivity.id, + name = savedActivity.name, + description = savedActivity.description, + open = savedActivity.open, + closed = savedActivity.closed, + start = savedActivity.start, + end = savedActivity.end, + price = savedActivity.price, + reductionFactor = 3.0, + siblingReduction = 10.0, + registrationLimit = 2, // Trying to lower below 3 registrations! + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null, + restrictions = emptyList() + ) + + val exception = assertThrows(IllegalStateException::class.java) { + activityService.mergeActivityDTOChanges(savedActivity.id!!, dto) + } + + assertEquals("The registration limit cannot be lowered below the current registration count!", exception.message) + } + + @Test + fun `cancelActivity should mark activity as cancelled and initiate refunds`() { + testActivity.open = LocalDateTime.now().minusDays(5) + testActivity.closed = LocalDateTime.now().plusDays(5) + val savedActivity = activityRepository.save(testActivity) + activityRepository.flush() + + // Create a paid registration + val registration = ActivityRegistration(testUser, savedActivity, savedActivity.price).apply { + paid = true + paymentId = "payment-123" + } + registrationRepository.save(registration) + registrationRepository.flush() + + activityService.cancelActivity(savedActivity.id!!) + + val cancelledActivity = activityRepository.findById(savedActivity.id!!).get() + assertTrue(cancelledActivity.cancelled) + } + + @Test + fun `cancelActivity should not allow cancelling already cancelled activity`() { + testActivity.cancelled = true + val savedActivity = activityRepository.save(testActivity) + activityRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + activityService.cancelActivity(savedActivity.id!!) + } + + assertEquals("This activity is already cancelled!", exception.message) + } + + @Test + fun `saveActivityDTO should create activity with address`() { + val address = Address().apply { + street = "Test Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + + val activityWithAddress = Activity().apply { + name = "Activity with Address" + description = "Test" + open = LocalDateTime.now().plusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + this.address = address + } + + val saved = activityRepository.save(activityWithAddress) + val retrieved = activityService.getActivityDTOById(saved.id!!) + + assertNotNull(retrieved.address) + assertEquals("Test Street", retrieved.address?.street) + assertEquals("123", retrieved.address?.number) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/event/EventRegistrationServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/event/EventRegistrationServiceIntegrationTest.kt new file mode 100644 index 0000000..64eaacd --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/event/EventRegistrationServiceIntegrationTest.kt @@ -0,0 +1,353 @@ +package be.sgl.backend.service.event + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.EventRegistrationAttemptData +import be.sgl.backend.entity.registrable.event.Event +import be.sgl.backend.entity.registrable.event.EventRegistration +import be.sgl.backend.repository.event.EventRegistrationRepository +import be.sgl.backend.repository.event.EventRepository +import be.sgl.backend.service.exception.EventRegistrationNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDateTime + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class EventRegistrationServiceIntegrationTest { + + @Autowired + private lateinit var registrationService: EventRegistrationService + + @Autowired + private lateinit var eventRepository: EventRepository + + @Autowired + private lateinit var registrationRepository: EventRegistrationRepository + + private lateinit var testEvent: Event + + @BeforeEach + fun setup() { + registrationRepository.deleteAll() + eventRepository.deleteAll() + + testEvent = Event().apply { + name = "Test Event" + description = "Test Description" + open = LocalDateTime.now().minusDays(1) // Already open + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + needsMobile = true + cancellable = true + sendConfirmation = true + sendCompleteConfirmation = false + } + testEvent = eventRepository.save(testEvent) + } + + @Test + fun `createPaymentForEvent should fail without mobile when needsMobile is true`() { + val attempt = EventRegistrationAttemptData( + name = "Doe", + firstName = "John", + email = "john@example.com", + mobile = null, // Missing mobile + additionalData = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.createPaymentForEvent(testEvent.id!!, attempt, null) + } + + assertEquals("No valid mobile number provided!", exception.message) + } + + @Test + fun `createPaymentForEvent should succeed with mobile when needsMobile is true`() { + val attempt = EventRegistrationAttemptData( + name = "Doe", + firstName = "John", + email = "john@example.com", + mobile = "0123456789", + additionalData = null + ) + + val checkoutUrl = registrationService.createPaymentForEvent(testEvent.id!!, attempt, null) + + assertNotNull(checkoutUrl) + assertTrue(checkoutUrl.isNotBlank()) + + // Verify registration was created + val registrations = registrationRepository.findAll() + assertEquals(1, registrations.size) + assertEquals("Doe", registrations[0].name) + assertEquals("john@example.com", registrations[0].email) + } + + @Test + fun `createPaymentForEvent should calculate price with additional data`() { + testEvent.additionalFormRule = "nights * 10" + eventRepository.save(testEvent) + + val attempt = EventRegistrationAttemptData( + name = "Doe", + firstName = "John", + email = "john@example.com", + mobile = "0123456789", + additionalData = "{\"nights\":\"3\"}" + ) + + registrationService.createPaymentForEvent(testEvent.id!!, attempt, null) + + val registration = registrationRepository.findAll()[0] + assertEquals(80.0, registration.price) // 50 base + 30 (3 nights * 10) + } + + @Test + fun `getAllRegistrationsForEvent should return only paid registrations`() { + val paidReg = EventRegistration().apply { + subscribable = testEvent + name = "Paid" + firstName = "User" + email = "paid@example.com" + mobile = "0123456789" + price = testEvent.price + paid = true + } + + val unpaidReg = EventRegistration().apply { + subscribable = testEvent + name = "Unpaid" + firstName = "User" + email = "unpaid@example.com" + mobile = "0987654321" + price = testEvent.price + paid = false + } + + registrationRepository.save(paidReg) + registrationRepository.save(unpaidReg) + registrationRepository.flush() + + val registrations = registrationService.getAllRegistrationsForEvent(testEvent.id!!) + + assertEquals(1, registrations.size) + assertEquals("paid@example.com", registrations[0].email) + } + + @Test + fun `markRegistrationAsCompleted should fail for unpaid registration`() { + val unpaidReg = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = false + } + val saved = registrationRepository.save(unpaidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.markRegistrationAsCompleted(saved.id!!) + } + + assertEquals("Only a paid event can be marked as completed!", exception.message) + } + + @Test + fun `markRegistrationAsCompleted should fail if event hasn't started yet`() { + testEvent.start = LocalDateTime.now().plusDays(5) // Event in future + eventRepository.save(testEvent) + + val paidReg = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = true + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.markRegistrationAsCompleted(saved.id!!) + } + + assertEquals("Registrations can only be completed starting one hour before the event!", exception.message) + } + + @Test + fun `markRegistrationAsCompleted should succeed for paid registration near event start`() { + testEvent.start = LocalDateTime.now().minusMinutes(30) // Started 30 min ago + testEvent.end = LocalDateTime.now().plusHours(2) + eventRepository.save(testEvent) + + val paidReg = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = true + completed = false + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + registrationService.markRegistrationAsCompleted(saved.id!!) + + val updated = registrationRepository.findById(saved.id!!).get() + assertTrue(updated.completed) + } + + @Test + fun `markRegistrationAsCompleted should not fail if already completed`() { + testEvent.start = LocalDateTime.now().minusMinutes(30) + testEvent.end = LocalDateTime.now().plusHours(2) + eventRepository.save(testEvent) + + val paidReg = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = true + completed = true // Already completed + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + // Should not throw exception, just log warning + assertDoesNotThrow { + registrationService.markRegistrationAsCompleted(saved.id!!) + } + + val registration = registrationRepository.findById(saved.id!!).get() + assertTrue(registration.completed) + } + + @Test + fun `cancelRegistration should fail for unpaid registration`() { + val unpaidReg = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = false + } + val saved = registrationRepository.save(unpaidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.cancelRegistration(saved.id!!) + } + + assertEquals("Only a paid event registration can be cancelled!", exception.message) + } + + @Test + fun `cancelRegistration should fail when registrations are closed`() { + testEvent.closed = LocalDateTime.now().minusDays(1) // Already closed + eventRepository.save(testEvent) + + val paidReg = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = true + } + val saved = registrationRepository.save(paidReg) + registrationRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + registrationService.cancelRegistration(saved.id!!) + } + + assertEquals("Cancellation is only possible when registrations are still open!", exception.message) + } + + @Test + fun `getEventRegistrationDTOById should return registration when exists`() { + val registration = EventRegistration().apply { + subscribable = testEvent + name = "User" + firstName = "Test" + email = "test@example.com" + mobile = "0123456789" + price = testEvent.price + paid = true + } + val saved = registrationRepository.save(registration) + registrationRepository.flush() + + val result = registrationService.getEventRegistrationDTOById(saved.id!!) + + assertNotNull(result) + assertEquals("test@example.com", result?.email) + } + + @Test + fun `getEventRegistrationDTOById should return null when not found`() { + val result = registrationService.getEventRegistrationDTOById(999) + + assertNull(result) + } + + @Test + fun `createPaymentForEvent should allow registration without mobile when not needed`() { + testEvent.needsMobile = false + eventRepository.save(testEvent) + + val attempt = EventRegistrationAttemptData( + name = "Doe", + firstName = "John", + email = "john@example.com", + mobile = null, // No mobile, but that's OK now + additionalData = null + ) + + val checkoutUrl = registrationService.createPaymentForEvent(testEvent.id!!, attempt, null) + + assertNotNull(checkoutUrl) + } + + @Test + fun `createPaymentForEvent should handle additional data with complex expressions`() { + testEvent.additionalFormRule = "adults * 20 + children * 10 + (lunch ? 15 : 0)" + eventRepository.save(testEvent) + + val attempt = EventRegistrationAttemptData( + name = "Doe", + firstName = "John", + email = "john@example.com", + mobile = "0123456789", + additionalData = "{\"adults\":\"2\",\"children\":\"3\",\"lunch\":\"true\"}" + ) + + registrationService.createPaymentForEvent(testEvent.id!!, attempt, null) + + val registration = registrationRepository.findAll()[0] + // Base: 50 + (2*20 + 3*10 + 15) = 50 + (40 + 30 + 15) = 135 + assertEquals(135.0, registration.price) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/event/EventServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/event/EventServiceIntegrationTest.kt new file mode 100644 index 0000000..e385bcd --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/event/EventServiceIntegrationTest.kt @@ -0,0 +1,384 @@ +package be.sgl.backend.service.event + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.EventDTO +import be.sgl.backend.entity.registrable.event.Event +import be.sgl.backend.entity.registrable.event.EventRegistration +import be.sgl.backend.repository.event.EventRegistrationRepository +import be.sgl.backend.repository.event.EventRepository +import be.sgl.backend.service.exception.EventNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDateTime + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class EventServiceIntegrationTest { + + @Autowired + private lateinit var eventService: EventService + + @Autowired + private lateinit var eventRepository: EventRepository + + @Autowired + private lateinit var registrationRepository: EventRegistrationRepository + + private lateinit var testEvent: Event + + @BeforeEach + fun setup() { + registrationRepository.deleteAll() + eventRepository.deleteAll() + + testEvent = Event().apply { + name = "Test Event" + description = "Test Description" + open = LocalDateTime.now().plusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + needsMobile = true + cancellable = true + sendConfirmation = true + sendCompleteConfirmation = false + } + } + + @Test + fun `getAllEvents should return all events with registration data`() { + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val events = eventService.getAllEvents() + + assertNotNull(events) + assertTrue(events.any { it.event.id == saved.id }) + } + + @Test + fun `getVisibleEvents should only return non-cancelled events`() { + val visibleEvent = testEvent + val cancelledEvent = Event().apply { + name = "Cancelled Event" + description = "Cancelled" + open = LocalDateTime.now().plusDays(1) + closed = LocalDateTime.now().plusDays(7) + start = LocalDateTime.now().plusDays(10) + end = LocalDateTime.now().plusDays(12) + price = 50.0 + cancelled = true + } + + eventRepository.save(visibleEvent) + eventRepository.save(cancelledEvent) + eventRepository.flush() + + val events = eventService.getVisibleEvents() + + assertTrue(events.any { it.name == "Test Event" }) + assertFalse(events.any { it.name == "Cancelled Event" }) + } + + @Test + fun `saveEventDTO should validate date sequence`() { + val dto = EventDTO( + id = null, + name = "Invalid Event", + description = "Test", + open = LocalDateTime.now().plusDays(10), + closed = LocalDateTime.now().plusDays(5), // Closed before open! + start = LocalDateTime.now().plusDays(15), + end = LocalDateTime.now().plusDays(20), + price = 50.0, + needsMobile = true, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.saveEventDTO(dto) + } + + assertEquals("The closure of registrations should be after the opening of registrations!", exception.message) + } + + @Test + fun `saveEventDTO should validate closed before start`() { + val dto = EventDTO( + id = null, + name = "Invalid Event", + description = "Test", + open = LocalDateTime.now().plusDays(5), + closed = LocalDateTime.now().plusDays(10), + start = LocalDateTime.now().plusDays(8), // Start before closed! + end = LocalDateTime.now().plusDays(20), + price = 50.0, + needsMobile = true, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.saveEventDTO(dto) + } + + assertEquals("The start date of an event should be after the closure of registrations!", exception.message) + } + + @Test + fun `saveEventDTO should validate start before end`() { + val dto = EventDTO( + id = null, + name = "Invalid Event", + description = "Test", + open = LocalDateTime.now().plusDays(5), + closed = LocalDateTime.now().plusDays(10), + start = LocalDateTime.now().plusDays(20), + end = LocalDateTime.now().plusDays(15), // End before start! + price = 50.0, + needsMobile = true, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.saveEventDTO(dto) + } + + assertEquals("The start date of an event should be before its end date!", exception.message) + } + + @Test + fun `mergeEventDTOChanges should not allow editing cancelled event`() { + testEvent.cancelled = true + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val dto = EventDTO( + id = saved.id, + name = "Updated Event", + description = "Updated", + open = testEvent.open, + closed = testEvent.closed, + start = testEvent.start, + end = testEvent.end, + price = 60.0, + needsMobile = true, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.mergeEventDTOChanges(saved.id!!, dto) + } + + assertEquals("A cancelled event cannot be edited anymore!", exception.message) + } + + @Test + fun `mergeEventDTOChanges should allow full edit when not yet open`() { + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val dto = EventDTO( + id = saved.id, + name = "Updated Event", + description = "Updated Description", + open = testEvent.open, + closed = testEvent.closed, + start = testEvent.start, + end = testEvent.end, + price = 75.0, + needsMobile = false, + registrationLimit = 100, + address = null, + additionalForm = "{\"field\": \"value\"}", + additionalFormRule = "price + 10", + cancellable = false, + sendConfirmation = false, + sendCompleteConfirmation = true, + communicationCC = "cc@example.com" + ) + + val updated = eventService.mergeEventDTOChanges(saved.id!!, dto) + + assertEquals("Updated Event", updated.name) + assertEquals(75.0, updated.price) + assertFalse(updated.needsMobile) + assertEquals(100, updated.registrationLimit) + } + + @Test + fun `mergeEventDTOChanges should not allow lowering limit below registrations`() { + testEvent.registrationLimit = 10 + testEvent.open = LocalDateTime.now().minusDays(5) // Already open + testEvent.closed = LocalDateTime.now().plusDays(5) + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + // Create 3 paid registrations + repeat(3) { index -> + val registration = EventRegistration().apply { + subscribable = saved + name = "User$index" + firstName = "Test" + email = "user$index@example.com" + price = saved.price + paid = true + } + registrationRepository.save(registration) + } + registrationRepository.flush() + + val dto = EventDTO( + id = saved.id, + name = saved.name, + description = saved.description, + open = saved.open, + closed = saved.closed, + start = saved.start, + end = saved.end, + price = saved.price, + needsMobile = saved.needsMobile, + registrationLimit = 2, // Trying to lower below 3 registrations! + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = true, + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.mergeEventDTOChanges(saved.id!!, dto) + } + + assertEquals("The registration limit cannot be lowered below the current registration count!", exception.message) + } + + @Test + fun `cancelEvent should mark event as cancelled`() { + testEvent.open = LocalDateTime.now().minusDays(5) + testEvent.closed = LocalDateTime.now().plusDays(5) + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + eventService.cancelEvent(saved.id!!) + + val cancelled = eventRepository.findById(saved.id!!).get() + assertTrue(cancelled.cancelled) + } + + @Test + fun `cancelEvent should not allow cancelling already cancelled event`() { + testEvent.cancelled = true + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.cancelEvent(saved.id!!) + } + + assertEquals("This event is already cancelled!", exception.message) + } + + @Test + fun `cancelEvent should not allow cancelling started event`() { + testEvent.open = LocalDateTime.now().minusDays(15) + testEvent.closed = LocalDateTime.now().minusDays(10) + testEvent.start = LocalDateTime.now().minusDays(5) + testEvent.end = LocalDateTime.now().plusDays(1) + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.cancelEvent(saved.id!!) + } + + assertEquals("A started event cannot be cancelled anymore!", exception.message) + } + + @Test + fun `getEventDTOById should return event when exists`() { + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val result = eventService.getEventDTOById(saved.id!!) + + assertNotNull(result) + assertEquals("Test Event", result.name) + } + + @Test + fun `getEventDTOById should throw exception when not found`() { + assertThrows(EventNotFoundException::class.java) { + eventService.getEventDTOById(999) + } + } + + @Test + fun `mergeEventDTOChanges should not allow making previously cancellable event uncancellable`() { + testEvent.cancellable = true + val saved = eventRepository.save(testEvent) + eventRepository.flush() + + val dto = EventDTO( + id = saved.id, + name = saved.name, + description = saved.description, + open = saved.open, + closed = saved.closed, + start = saved.start, + end = saved.end, + price = saved.price, + needsMobile = saved.needsMobile, + registrationLimit = null, + address = null, + additionalForm = null, + additionalFormRule = null, + cancellable = false, // Trying to make it uncancellable! + sendConfirmation = true, + sendCompleteConfirmation = false, + communicationCC = null + ) + + val exception = assertThrows(IllegalStateException::class.java) { + eventService.mergeEventDTOChanges(saved.id!!, dto) + } + + assertEquals("A previously cancellable event cannot be made uncancellable!", exception.message) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/membership/MembershipPeriodServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/membership/MembershipPeriodServiceIntegrationTest.kt new file mode 100644 index 0000000..d9a201e --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/membership/MembershipPeriodServiceIntegrationTest.kt @@ -0,0 +1,425 @@ +package be.sgl.backend.service.membership + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.MembershipPeriodDTO +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.membership.Membership +import be.sgl.backend.entity.membership.MembershipPeriod +import be.sgl.backend.entity.membership.MembershipRestriction +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.membership.MembershipPeriodRepository +import be.sgl.backend.repository.membership.MembershipRepository +import be.sgl.backend.repository.user.UserRepository +import be.sgl.backend.service.exception.MembershipPeriodNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class MembershipPeriodServiceIntegrationTest { + + @Autowired + private lateinit var periodService: MembershipPeriodService + + @Autowired + private lateinit var periodRepository: MembershipPeriodRepository + + @Autowired + private lateinit var membershipRepository: MembershipRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Autowired + private lateinit var userRepository: UserRepository + + private lateinit var testBranch: Branch + + @BeforeEach + fun setup() { + membershipRepository.deleteAll() + userRepository.deleteAll() + branchRepository.deleteAll() + periodRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + } + + @Test + fun `getAllMembershipPeriods should return all periods with member counts`() { + val period1 = MembershipPeriod().apply { + name = "Period 1" + start = LocalDate.of(2023, 9, 1) + end = LocalDate.of(2024, 8, 31) + basePrice = 100.0 + } + periodRepository.save(period1) + + val period2 = MembershipPeriod().apply { + name = "Period 2" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + basePrice = 110.0 + } + periodRepository.save(period2) + periodRepository.flush() + + val periods = periodService.getAllMembershipPeriods() + + assertTrue(periods.size >= 2) + assertTrue(periods.any { it.period.name == "Period 1" }) + assertTrue(periods.any { it.period.name == "Period 2" }) + } + + @Test + fun `getAllMembershipPeriods should include paid member count`() { + val period = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.now().minusMonths(2) + end = LocalDate.now().plusMonths(10) + basePrice = 100.0 + } + val savedPeriod = periodRepository.save(period) + + val user1 = User().apply { + username = "user1" + firstName = "User" + name = "One" + email = "user1@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(user1) + + val user2 = User().apply { + username = "user2" + firstName = "User" + name = "Two" + email = "user2@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(user2) + + val membership1 = Membership(savedPeriod, user1, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership1) + + val membership2 = Membership(savedPeriod, user2, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership2) + + // Unpaid membership should not be counted + val user3 = User().apply { + username = "user3" + firstName = "User" + name = "Three" + email = "user3@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(user3) + + val unpaidMembership = Membership(savedPeriod, user3, testBranch, 100.0).apply { + paid = false + } + membershipRepository.save(unpaidMembership) + membershipRepository.flush() + + val periods = periodService.getAllMembershipPeriods() + + val testPeriod = periods.find { it.period.name == "Test Period" } + assertNotNull(testPeriod) + assertEquals(2, testPeriod?.memberships?.size) + } + + @Test + fun `getMembershipPeriodDTOById should return period when exists`() { + val period = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + basePrice = 100.0 + } + val saved = periodRepository.save(period) + periodRepository.flush() + + val result = periodService.getMembershipPeriodDTOById(saved.id!!) + + assertNotNull(result) + assertEquals("Test Period", result.name) + assertEquals(LocalDate.of(2024, 9, 1), result.start) + assertEquals(LocalDate.of(2025, 8, 31), result.end) + } + + @Test + fun `getMembershipPeriodDTOById should throw exception when not found`() { + assertThrows(MembershipPeriodNotFoundException::class.java) { + periodService.getMembershipPeriodDTOById(999) + } + } + + @Test + fun `getCurrentMembershipPeriod should return active period`() { + val currentPeriod = MembershipPeriod().apply { + name = "Current Period" + start = LocalDate.now().minusMonths(2) + end = LocalDate.now().plusMonths(10) + basePrice = 100.0 + } + periodRepository.save(currentPeriod) + periodRepository.flush() + + val result = periodService.getCurrentMembershipPeriod() + + assertNotNull(result) + assertEquals("Current Period", result.name) + } + + @Test + fun `saveMembershipPeriodDTO should create new period`() { + val dto = MembershipPeriodDTO( + id = null, + name = "New Period", + start = LocalDate.of(2025, 9, 1), + end = LocalDate.of(2026, 8, 31), + basePrice = 120.0, + restrictions = emptyList() + ) + + val saved = periodService.saveMembershipPeriodDTO(dto) + + assertNotNull(saved.id) + assertEquals("New Period", saved.name) + assertEquals(120.0, saved.basePrice) + + val persisted = periodRepository.findById(saved.id!!).get() + assertEquals("New Period", persisted.name) + } + + @Test + fun `saveMembershipPeriodDTO should create period with restrictions`() { + val restriction1 = MembershipRestriction( + testBranch, + LocalDate.of(2025, 9, 1), + 100 + ) + + val period = MembershipPeriod().apply { + name = "Period with Restrictions" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 100.0 + restrictions.add(restriction1) + } + + val saved = periodRepository.save(period) + periodRepository.flush() + + val result = periodService.getMembershipPeriodDTOById(saved.id!!) + + assertNotNull(result) + assertEquals(1, result.restrictions.size) + } + + @Test + fun `membership period should validate restriction dates`() { + val period = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 100.0 + } + + // Restriction with date before period start should fail validation + val invalidRestriction = MembershipRestriction( + testBranch, + LocalDate.of(2025, 8, 1), // Before period start + 100 + ) + period.restrictions.add(invalidRestriction) + + val exception = assertThrows(IllegalStateException::class.java) { + period.validateRestrictions() + } + + assertTrue(exception.message!!.contains("restriction") || exception.message!!.contains("before")) + } + + @Test + fun `membership period should validate restriction limits`() { + val period = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 100.0 + } + + // Restriction with non-positive limit should fail validation + val invalidRestriction = MembershipRestriction( + testBranch, + LocalDate.of(2025, 10, 1), + 0 // Non-positive limit + ) + period.restrictions.add(invalidRestriction) + + val exception = assertThrows(IllegalStateException::class.java) { + period.validateRestrictions() + } + + assertTrue(exception.message!!.contains("limit") || exception.message!!.contains("positive")) + } + + @Test + fun `periods should be ordered by start date descending`() { + val period1 = MembershipPeriod().apply { + name = "2023-2024" + start = LocalDate.of(2023, 9, 1) + end = LocalDate.of(2024, 8, 31) + basePrice = 100.0 + } + periodRepository.save(period1) + + val period2 = MembershipPeriod().apply { + name = "2024-2025" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + basePrice = 110.0 + } + periodRepository.save(period2) + + val period3 = MembershipPeriod().apply { + name = "2025-2026" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 120.0 + } + periodRepository.save(period3) + periodRepository.flush() + + val periods = periodService.getAllMembershipPeriods() + + assertTrue(periods.size >= 3) + // Most recent period should be first + val firstPeriod = periods.find { it.period.name == "2025-2026" } + val lastPeriod = periods.find { it.period.name == "2023-2024" } + assertNotNull(firstPeriod) + assertNotNull(lastPeriod) + + val firstIndex = periods.indexOf(firstPeriod) + val lastIndex = periods.indexOf(lastPeriod) + assertTrue(firstIndex < lastIndex) + } + + @Test + fun `membership period with multiple branches should track restrictions separately`() { + val branch2 = Branch().apply { + name = "Branch 2" + email = "branch2@example.com" + minimumAge = 12 + maximumAge = 16 + sex = Sex.UNKNOWN + description = "Test 2" + law = "Law" + image = "test2.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(branch2) + + val period = MembershipPeriod().apply { + name = "Multi-Branch Period" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 100.0 + } + + val restriction1 = MembershipRestriction(testBranch, LocalDate.of(2025, 10, 1), 50) + val restriction2 = MembershipRestriction(branch2, LocalDate.of(2025, 10, 15), 75) + + period.restrictions.add(restriction1) + period.restrictions.add(restriction2) + + val saved = periodRepository.save(period) + periodRepository.flush() + + val result = periodService.getMembershipPeriodDTOById(saved.id!!) + + assertEquals(2, result.restrictions.size) + assertTrue(result.restrictions.any { it.branch.name == "Test Branch" && it.maxCount == 50 }) + assertTrue(result.restrictions.any { it.branch.name == "Branch 2" && it.maxCount == 75 }) + } + + @Test + fun `period toString should format dates correctly`() { + val period = MembershipPeriod().apply { + name = "2024-2025" + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + basePrice = 100.0 + } + + val toString = period.toString() + + assertTrue(toString.contains("2024") || toString.contains("2025")) + } + + @Test + fun `period with no restrictions should be valid`() { + val period = MembershipPeriod().apply { + name = "No Restrictions" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 100.0 + } + + assertDoesNotThrow { + period.validateRestrictions() + } + } + + @Test + fun `saveMembershipPeriodDTO should set bidirectional relationship with restrictions`() { + val restriction = MembershipRestriction( + testBranch, + LocalDate.of(2025, 10, 1), + 100 + ) + + val period = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.of(2025, 9, 1) + end = LocalDate.of(2026, 8, 31) + basePrice = 100.0 + restrictions.add(restriction) + } + + val saved = periodRepository.save(period) + periodRepository.flush() + + val retrieved = periodRepository.findById(saved.id!!).get() + assertEquals(1, retrieved.restrictions.size) + assertEquals(saved.id, retrieved.restrictions[0].period?.id) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/membership/MembershipServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/membership/MembershipServiceIntegrationTest.kt new file mode 100644 index 0000000..061e034 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/membership/MembershipServiceIntegrationTest.kt @@ -0,0 +1,396 @@ +package be.sgl.backend.service.membership + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.UserRegistrationDTO +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.membership.Membership +import be.sgl.backend.entity.membership.MembershipPeriod +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.membership.MembershipPeriodRepository +import be.sgl.backend.repository.membership.MembershipRepository +import be.sgl.backend.repository.user.UserRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class MembershipServiceIntegrationTest { + + @Autowired + private lateinit var membershipService: MembershipService + + @Autowired + private lateinit var membershipRepository: MembershipRepository + + @Autowired + private lateinit var membershipPeriodRepository: MembershipPeriodRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Autowired + private lateinit var userRepository: UserRepository + + private lateinit var testBranch: Branch + private lateinit var testPeriod: MembershipPeriod + private lateinit var testUser: User + + @BeforeEach + fun setup() { + membershipRepository.deleteAll() + userRepository.deleteAll() + branchRepository.deleteAll() + membershipPeriodRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + + testPeriod = MembershipPeriod().apply { + name = "Test Period" + start = LocalDate.now().minusMonths(2) + end = LocalDate.now().plusMonths(10) + basePrice = 100.0 + } + testPeriod = membershipPeriodRepository.save(testPeriod) + + testUser = User().apply { + username = "testuser" + firstName = "Test" + name = "User" + email = "testuser@example.com" + birthdate = LocalDate.of(2014, 1, 1) + sex = Sex.UNKNOWN + } + testUser = userRepository.save(testUser) + } + + @Test + fun `getAllMembershipsForUser should return all user memberships`() { + val membership1 = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership1) + + val period2 = MembershipPeriod().apply { + name = "Period 2" + start = LocalDate.now().plusYears(1) + end = LocalDate.now().plusYears(2) + basePrice = 110.0 + } + membershipPeriodRepository.save(period2) + + val membership2 = Membership(period2, testUser, testBranch, 110.0).apply { + paid = true + } + membershipRepository.save(membership2) + membershipRepository.flush() + + val memberships = membershipService.getAllMembershipsForUser(testUser.username!!) + + assertTrue(memberships.size >= 2) + assertTrue(memberships.any { it.period.name == "Test Period" }) + assertTrue(memberships.any { it.period.name == "Period 2" }) + } + + @Test + fun `getCurrentMembershipForUser should return active membership`() { + val membership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership) + membershipRepository.flush() + + val current = membershipService.getCurrentMembershipForUser(testUser.username!!) + + assertNotNull(current) + assertEquals(testPeriod.name, current?.period?.name) + assertEquals(testUser.email, current?.user?.email) + } + + @Test + fun `getCurrentMembershipForUser should return null when no active membership`() { + val futurePeriod = MembershipPeriod().apply { + name = "Future Period" + start = LocalDate.now().plusYears(1) + end = LocalDate.now().plusYears(2) + basePrice = 100.0 + } + membershipPeriodRepository.save(futurePeriod) + + val membership = Membership(futurePeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership) + membershipRepository.flush() + + val current = membershipService.getCurrentMembershipForUser(testUser.username!!) + + assertNull(current) + } + + @Test + fun `getCurrentMembershipsForBranch should return all current memberships for branch`() { + val user1 = User().apply { + username = "user1" + firstName = "User" + name = "One" + email = "user1@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(user1) + + val user2 = User().apply { + username = "user2" + firstName = "User" + name = "Two" + email = "user2@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(user2) + + val membership1 = Membership(testPeriod, user1, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership1) + + val membership2 = Membership(testPeriod, user2, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership2) + membershipRepository.flush() + + val memberships = membershipService.getCurrentMembershipsForBranch(testBranch.id) + + assertTrue(memberships.size >= 2) + assertTrue(memberships.any { it.user.email == "user1@example.com" }) + assertTrue(memberships.any { it.user.email == "user2@example.com" }) + } + + @Test + fun `getCurrentMembershipsForBranch with null should return all current memberships`() { + val membership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership) + membershipRepository.flush() + + val memberships = membershipService.getCurrentMembershipsForBranch(null) + + assertTrue(memberships.size >= 1) + } + + @Test + fun `createMembershipForExistingUser should fail when user already has active membership`() { + val membership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership) + membershipRepository.flush() + + val exception = assertThrows(IllegalStateException::class.java) { + membershipService.createMembershipForExistingUser(testUser.username!!) + } + + assertTrue(exception.message!!.contains("already an active membership")) + } + + @Test + fun `getMembershipDTOById should return membership when exists`() { + val membership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + val saved = membershipRepository.save(membership) + membershipRepository.flush() + + val result = membershipService.getMembershipDTOById(saved.id!!) + + assertNotNull(result) + assertEquals(testUser.email, result?.user?.email) + assertEquals(testBranch.name, result?.branch?.name) + } + + @Test + fun `getMembershipDTOById should return null when not found`() { + val result = membershipService.getMembershipDTOById(999) + + assertNull(result) + } + + @Test + fun `membership should only be active during period dates`() { + // Create past period + val pastPeriod = MembershipPeriod().apply { + name = "Past Period" + start = LocalDate.now().minusYears(2) + end = LocalDate.now().minusYears(1) + basePrice = 90.0 + } + membershipPeriodRepository.save(pastPeriod) + + val pastMembership = Membership(pastPeriod, testUser, testBranch, 90.0).apply { + paid = true + } + membershipRepository.save(pastMembership) + + val currentMembership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(currentMembership) + membershipRepository.flush() + + val current = membershipService.getCurrentMembershipForUser(testUser.username!!) + + assertNotNull(current) + assertEquals(testPeriod.name, current?.period?.name) + assertEquals("Test Period", current?.period?.name) + } + + @Test + fun `getCurrentMembershipsForBranch should only return paid memberships`() { + val paidMembership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(paidMembership) + + val unpaidUser = User().apply { + username = "unpaiduser" + firstName = "Unpaid" + name = "User" + email = "unpaid@example.com" + birthdate = LocalDate.of(2014, 1, 1) + } + userRepository.save(unpaidUser) + + val unpaidMembership = Membership(testPeriod, unpaidUser, testBranch, 100.0).apply { + paid = false + } + membershipRepository.save(unpaidMembership) + membershipRepository.flush() + + val memberships = membershipService.getCurrentMembershipsForBranch(testBranch.id) + + // Should only contain paid membership + assertTrue(memberships.any { it.user.email == testUser.email }) + assertFalse(memberships.any { it.user.email == unpaidUser.email }) + } + + @Test + fun `membership for multiple branches should be tracked separately`() { + val branch2 = Branch().apply { + name = "Branch 2" + email = "branch2@example.com" + minimumAge = 12 + maximumAge = 16 + sex = Sex.UNKNOWN + description = "Test 2" + law = "Law" + image = "test2.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branchRepository.save(branch2) + + val membership1 = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(membership1) + + val user2 = User().apply { + username = "user2" + firstName = "User" + name = "Two" + email = "user2@example.com" + birthdate = LocalDate.of(2010, 1, 1) + } + userRepository.save(user2) + + val membership2 = Membership(testPeriod, user2, branch2, 100.0).apply { + paid = true + } + membershipRepository.save(membership2) + membershipRepository.flush() + + val branch1Memberships = membershipService.getCurrentMembershipsForBranch(testBranch.id) + val branch2Memberships = membershipService.getCurrentMembershipsForBranch(branch2.id) + + assertTrue(branch1Memberships.any { it.user.email == testUser.email }) + assertFalse(branch1Memberships.any { it.user.email == user2.email }) + + assertTrue(branch2Memberships.any { it.user.email == user2.email }) + assertFalse(branch2Memberships.any { it.user.email == testUser.email }) + } + + @Test + fun `membership with reduced price should be saved correctly`() { + val reducedUser = User().apply { + username = "reduceduser" + firstName = "Reduced" + name = "User" + email = "reduced@example.com" + birthdate = LocalDate.of(2014, 1, 1) + hasReduction = true + } + userRepository.save(reducedUser) + + // Price reduced by factor of 3: 100.0 / 3 = 33.33 + val membership = Membership(testPeriod, reducedUser, testBranch, 33.33).apply { + paid = true + } + membershipRepository.save(membership) + membershipRepository.flush() + + val result = membershipService.getMembershipDTOById(membership.id!!) + + assertNotNull(result) + assertEquals(33.33, result?.price) + } + + @Test + fun `getAllMembershipsForUser should include both paid and unpaid memberships`() { + val paidMembership = Membership(testPeriod, testUser, testBranch, 100.0).apply { + paid = true + } + membershipRepository.save(paidMembership) + + val period2 = MembershipPeriod().apply { + name = "Period 2" + start = LocalDate.now().plusYears(1) + end = LocalDate.now().plusYears(2) + basePrice = 110.0 + } + membershipPeriodRepository.save(period2) + + val unpaidMembership = Membership(period2, testUser, testBranch, 110.0).apply { + paid = false + } + membershipRepository.save(unpaidMembership) + membershipRepository.flush() + + val memberships = membershipService.getAllMembershipsForUser(testUser.username!!) + + assertTrue(memberships.size >= 2) + assertTrue(memberships.any { it.paid }) + assertTrue(memberships.any { !it.paid }) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/membership/ValidateAndCreateMembershipTest.kt b/src/test/kotlin/be/sgl/backend/service/membership/ValidateAndCreateMembershipTest.kt new file mode 100644 index 0000000..303a10f --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/membership/ValidateAndCreateMembershipTest.kt @@ -0,0 +1,323 @@ +package be.sgl.backend.service.membership + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.membership.MembershipPeriod +import be.sgl.backend.entity.membership.MembershipRestriction +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.membership.MembershipRepository +import be.sgl.backend.repository.user.SiblingRepository +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class ValidateAndCreateMembershipTest { + + @Autowired + private lateinit var validateAndCreateMembership: ValidateAndCreateMembership + + @Autowired + private lateinit var membershipRepository: MembershipRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + @Autowired + private lateinit var siblingRepository: SiblingRepository + + private lateinit var period: MembershipPeriod + private lateinit var branch: Branch + private lateinit var user: User + + @BeforeEach + fun setup() { + // Create a test branch + branch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 8 + sex = Sex.UNKNOWN + description = "Test branch description" + law = "Test law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + branch = branchRepository.save(branch) + + // Create a test membership period + period = MembershipPeriod().apply { + start = LocalDate.of(2024, 9, 1) + end = LocalDate.of(2025, 8, 31) + price = 100.0 + reductionFactor = 3.0 + siblingReduction = 10.0 + } + + // Create a test user + user = User().apply { + name = "Doe" + firstName = "John" + email = "john.doe@example.com" + birthdate = LocalDate.of(2017, 5, 15) // 7 years old + sex = Sex.MALE + } + } + + @Test + fun `execute should create membership with base price for regular user`() { + val membership = validateAndCreateMembership.execute(period, user) + + assertNotNull(membership) + assertEquals(user, membership.user) + assertEquals(period, membership.period) + assertEquals(branch, membership.branch) + assertEquals(100.0, membership.price) + } + + @Test + fun `execute should apply reduction for user with hasReduction flag`() { + user.hasReduction = true + + val membership = validateAndCreateMembership.execute(period, user) + + assertEquals(33.33, membership.price) // 100 / 3 rounded to 2 decimals + } + + @Test + fun `execute should apply sibling reduction when sibling has membership`() { + // Create sibling + val sibling = User().apply { + name = "Doe" + firstName = "Jane" + email = "jane.doe@example.com" + birthdate = LocalDate.of(2016, 3, 10) + sex = Sex.FEMALE + hasReduction = false + } + + // Save sibling first + siblingRepository.flush() + + // Create membership for sibling + val siblingMembership = validateAndCreateMembership.execute(period, sibling) + membershipRepository.save(siblingMembership) + membershipRepository.flush() + + // Register sibling relationship + val siblingRelation = be.sgl.backend.entity.user.SiblingRelation().apply { + this.user = user + this.sibling = sibling + } + siblingRepository.save(siblingRelation) + siblingRepository.flush() + + val membership = validateAndCreateMembership.execute(period, user) + + assertEquals(90.0, membership.price) // 100 - 10 sibling reduction + } + + @Test + fun `execute should not apply sibling reduction if sibling has reduction flag`() { + val sibling = User().apply { + name = "Doe" + firstName = "Jane" + email = "jane.doe@example.com" + birthdate = LocalDate.of(2016, 3, 10) + sex = Sex.FEMALE + hasReduction = true // Sibling has reduction + } + + siblingRepository.flush() + + val siblingMembership = validateAndCreateMembership.execute(period, sibling) + membershipRepository.save(siblingMembership) + membershipRepository.flush() + + val siblingRelation = be.sgl.backend.entity.user.SiblingRelation().apply { + this.user = user + this.sibling = sibling + } + siblingRepository.save(siblingRelation) + siblingRepository.flush() + + val membership = validateAndCreateMembership.execute(period, user) + + assertEquals(100.0, membership.price) // Full price, no sibling reduction + } + + @Test + fun `execute should respect period registration limit`() { + period.registrationLimit = 1 + + // Create first membership + val firstUser = User().apply { + name = "Smith" + firstName = "Alice" + email = "alice@example.com" + birthdate = LocalDate.of(2017, 1, 1) + sex = Sex.FEMALE + } + val firstMembership = validateAndCreateMembership.execute(period, firstUser) + membershipRepository.save(firstMembership) + membershipRepository.flush() + + // Try to create second membership - should fail + val exception = assertThrows(IllegalStateException::class.java) { + validateAndCreateMembership.execute(period, user) + } + + assertEquals("This period already has its maximum number of members!", exception.message) + } + + @Test + fun `execute should respect branch-specific registration limit`() { + val restriction = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.registrationLimit = 1 + } + period.restrictions.add(restriction) + + // Create first membership for this branch + val firstUser = User().apply { + name = "Smith" + firstName = "Alice" + email = "alice@example.com" + birthdate = LocalDate.of(2017, 1, 1) + sex = Sex.FEMALE + } + val firstMembership = validateAndCreateMembership.execute(period, firstUser) + membershipRepository.save(firstMembership) + membershipRepository.flush() + + // Try to create second membership - should fail + val exception = assertThrows(IllegalStateException::class.java) { + validateAndCreateMembership.execute(period, user) + } + + assertEquals("No more free membership spots for this branch!", exception.message) + } + + @Test + fun `execute should apply time-based price restriction`() { + val earlyBirdPrice = 80.0 + val earlyBirdDate = LocalDate.of(2024, 7, 1) + + val restriction = MembershipRestriction().apply { + this.period = period + this.alternativeStart = earlyBirdDate + this.alternativePrice = earlyBirdPrice + } + period.restrictions.add(restriction) + + val membershipBeforeDeadline = validateAndCreateMembership.execute( + period, user, at = LocalDate.of(2024, 7, 15) + ) + + assertEquals(earlyBirdPrice, membershipBeforeDeadline.price) + + val membershipAfterDeadline = validateAndCreateMembership.execute( + period, user, at = LocalDate.of(2024, 8, 15) + ) + + assertEquals(100.0, membershipAfterDeadline.price) // Back to base price + } + + @Test + fun `execute should apply branch-specific alternative price`() { + val branchSpecificPrice = 75.0 + + val restriction = MembershipRestriction().apply { + this.period = period + this.branch = branch + this.alternativePrice = branchSpecificPrice + } + period.restrictions.add(restriction) + + val membership = validateAndCreateMembership.execute(period, user) + + assertEquals(branchSpecificPrice, membership.price) + } + + @Test + fun `execute should throw exception when no branch matches user age and sex`() { + // Create user that doesn't fit any branch + val tooOldUser = User().apply { + name = "Old" + firstName = "Person" + email = "old@example.com" + birthdate = LocalDate.of(2000, 1, 1) // Too old for our test branch + sex = Sex.MALE + } + + val exception = assertThrows(IllegalStateException::class.java) { + validateAndCreateMembership.execute(period, tooOldUser) + } + + assertEquals("No active branch can be linked to a user of this age and sex!", exception.message) + } + + @Test + fun `execute should consider user age deviation`() { + // User is normally 7 years old, but with deviation of 2, effectively 9 + user.ageDeviation = 2 + + // This should fail because effective age (9) exceeds branch maximum (8) + val exception = assertThrows(IllegalStateException::class.java) { + validateAndCreateMembership.execute(period, user) + } + + assertEquals("No active branch can be linked to a user of this age and sex!", exception.message) + } + + @Test + fun `execute should calculate age at end of period`() { + // User born in May 2017, will be 8 at end of period (Aug 2025) + val youngUser = User().apply { + name = "Young" + firstName = "Person" + email = "young@example.com" + birthdate = LocalDate.of(2017, 5, 15) + sex = Sex.MALE + } + + val membership = validateAndCreateMembership.execute(period, youngUser) + + assertNotNull(membership) + assertEquals(branch, membership.branch) // Should still fit in branch + } + + @Test + fun `execute should prefer most recent time restriction`() { + val firstRestriction = MembershipRestriction().apply { + this.period = period + this.alternativeStart = LocalDate.of(2024, 6, 1) + this.alternativePrice = 90.0 + } + val secondRestriction = MembershipRestriction().apply { + this.period = period + this.alternativeStart = LocalDate.of(2024, 7, 1) + this.alternativePrice = 85.0 + } + period.restrictions.addAll(listOf(firstRestriction, secondRestriction)) + + val membership = validateAndCreateMembership.execute( + period, user, at = LocalDate.of(2024, 7, 15) + ) + + assertEquals(85.0, membership.price) // Should use most recent restriction + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/organization/OrganizationServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/organization/OrganizationServiceIntegrationTest.kt new file mode 100644 index 0000000..8481f80 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/organization/OrganizationServiceIntegrationTest.kt @@ -0,0 +1,331 @@ +package be.sgl.backend.service.organization + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.dto.RepresentativeDTO +import be.sgl.backend.entity.Address +import be.sgl.backend.entity.organization.ContactMethod +import be.sgl.backend.entity.organization.ContactMethodType +import be.sgl.backend.entity.organization.Organization +import be.sgl.backend.entity.organization.OrganizationType +import be.sgl.backend.entity.setting.Setting +import be.sgl.backend.entity.setting.SettingId +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.AddressRepository +import be.sgl.backend.repository.OrganizationRepository +import be.sgl.backend.repository.SettingRepository +import be.sgl.backend.repository.user.UserRepository +import be.sgl.backend.service.exception.OrganizationNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class OrganizationServiceIntegrationTest { + + @Autowired + private lateinit var organizationService: OrganizationService + + @Autowired + private lateinit var organizationRepository: OrganizationRepository + + @Autowired + private lateinit var addressRepository: AddressRepository + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var settingRepository: SettingRepository + + private lateinit var ownerOrg: Organization + private lateinit var certifierOrg: Organization + private lateinit var testAddress: Address + + @BeforeEach + fun setup() { + organizationRepository.deleteAll() + addressRepository.deleteAll() + userRepository.deleteAll() + settingRepository.deleteAll() + + testAddress = Address().apply { + street = "Test Street" + number = "123" + zipcode = "1000" + town = "Brussels" + country = "BE" + } + testAddress = addressRepository.save(testAddress) + + ownerOrg = Organization().apply { + name = "Owner Organization" + type = OrganizationType.OWNER + kbo = "1234567890" + address = testAddress + description = "Owner description" + } + ownerOrg = organizationRepository.save(ownerOrg) + + val certifierAddress = Address().apply { + street = "Certifier Street" + number = "456" + zipcode = "2000" + town = "Antwerp" + country = "BE" + } + addressRepository.save(certifierAddress) + + certifierOrg = Organization().apply { + name = "Certifier Organization" + type = OrganizationType.CERTIFIER + kbo = "0987654321" + address = certifierAddress + description = "Certifier description" + } + certifierOrg = organizationRepository.save(certifierOrg) + } + + @Test + fun `getOwner should return owner organization`() { + val owner = organizationService.getOwner() + + assertNotNull(owner) + assertEquals("Owner Organization", owner.name) + assertEquals(OrganizationType.OWNER, owner.type) + } + + @Test + fun `getCertifier should return certifier organization`() { + val certifier = organizationService.getCertifier() + + assertNotNull(certifier) + assertEquals("Certifier Organization", certifier.name) + assertEquals(OrganizationType.CERTIFIER, certifier.type) + } + + @Test + fun `organization should have address information`() { + val owner = organizationService.getOwner() + + assertNotNull(owner.address) + assertEquals("Test Street", owner.address.street) + assertEquals("123", owner.address.number) + assertEquals("1000", owner.address.zipcode) + assertEquals("Brussels", owner.address.town) + } + + @Test + fun `organization should include KBO number`() { + val owner = organizationService.getOwner() + + assertEquals("1234567890", owner.kbo) + } + + @Test + fun `organization should include description`() { + val owner = organizationService.getOwner() + + assertEquals("Owner description", owner.description) + } + + @Test + fun `organization getEmail should return email contact method`() { + val emailContact = ContactMethod(ownerOrg, ContactMethodType.EMAIL, "owner@example.com") + ownerOrg.contactMethods.add(emailContact) + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val retrieved = organizationRepository.findById(ownerOrg.id!!).get() + val email = retrieved.getEmail() + + assertEquals("owner@example.com", email) + } + + @Test + fun `organization getMobile should return mobile contact method`() { + val mobileContact = ContactMethod(ownerOrg, ContactMethodType.MOBILE, "0123456789") + ownerOrg.contactMethods.add(mobileContact) + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val retrieved = organizationRepository.findById(ownerOrg.id!!).get() + val mobile = retrieved.getMobile() + + assertEquals("0123456789", mobile) + } + + @Test + fun `organization should support multiple contact methods`() { + val emailContact = ContactMethod(ownerOrg, ContactMethodType.EMAIL, "owner@example.com") + val mobileContact = ContactMethod(ownerOrg, ContactMethodType.MOBILE, "0123456789") + val linkContact = ContactMethod(ownerOrg, ContactMethodType.LINK, "https://example.com") + + ownerOrg.contactMethods.add(emailContact) + ownerOrg.contactMethods.add(mobileContact) + ownerOrg.contactMethods.add(linkContact) + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val retrieved = organizationRepository.findById(ownerOrg.id!!).get() + + assertEquals(3, retrieved.contactMethods.size) + assertEquals("owner@example.com", retrieved.getEmail()) + assertEquals("0123456789", retrieved.getMobile()) + } + + @Test + fun `getRepresentativeDTO should return representative settings`() { + val testUser = User().apply { + username = "representative" + firstName = "Rep" + name = "User" + email = "rep@example.com" + birthdate = LocalDate.of(1980, 1, 1) + } + userRepository.save(testUser) + + settingRepository.save(Setting(SettingId.REPRESENTATIVE_USERNAME, "representative")) + settingRepository.save(Setting(SettingId.REPRESENTATIVE_TITLE, "Director")) + settingRepository.save(Setting(SettingId.REPRESENTATIVE_SIGNATURE, "signature.png")) + settingRepository.flush() + + val representative = organizationService.getRepresentativeDTO() + + assertEquals("representative", representative.username) + assertEquals("Director", representative.title) + assertEquals("signature.png", representative.signature) + } + + @Test + fun `getRepresentativeDTO should return null values when not configured`() { + val representative = organizationService.getRepresentativeDTO() + + assertNull(representative.username) + assertNull(representative.title) + assertNull(representative.signature) + } + + @Test + fun `owner and certifier should be separate organizations`() { + val owner = organizationService.getOwner() + val certifier = organizationService.getCertifier() + + assertNotEquals(owner.id, certifier.id) + assertNotEquals(owner.name, certifier.name) + } + + @Test + fun `organization should have unique type`() { + // Attempting to create another OWNER should fail + val duplicateOwner = Organization().apply { + name = "Duplicate Owner" + type = OrganizationType.OWNER + address = testAddress + } + + // The service checks for existing organization by type + // This would fail in saveOrganizationDTO with the check + } + + @Test + fun `organization without image should return null image`() { + ownerOrg.image = null + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val owner = organizationService.getOwner() + + assertNull(owner.image) + } + + @Test + fun `organization with image should return image path`() { + ownerOrg.image = "organization-logo.png" + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val owner = organizationService.getOwner() + + assertEquals("organization-logo.png", owner.image) + } + + @Test + fun `organization address should be fully populated`() { + val owner = organizationService.getOwner() + + assertNotNull(owner.address) + assertNotNull(owner.address.street) + assertNotNull(owner.address.number) + assertNotNull(owner.address.zipcode) + assertNotNull(owner.address.town) + assertNotNull(owner.address.country) + } + + @Test + fun `organization KBO should be optional`() { + ownerOrg.kbo = null + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val owner = organizationService.getOwner() + + assertNull(owner.kbo) + } + + @Test + fun `organization description should be optional`() { + ownerOrg.description = null + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val owner = organizationService.getOwner() + + assertNull(owner.description) + } + + @Test + fun `organization description should support long text`() { + val longDescription = "A".repeat(500) + ownerOrg.description = longDescription + organizationRepository.save(ownerOrg) + organizationRepository.flush() + + val owner = organizationService.getOwner() + + assertEquals(longDescription, owner.description) + assertEquals(500, owner.description?.length) + } + + @Test + fun `organization should track both types separately`() { + val allOrgs = organizationRepository.findAll() + + assertTrue(allOrgs.any { it.type == OrganizationType.OWNER }) + assertTrue(allOrgs.any { it.type == OrganizationType.CERTIFIER }) + assertEquals(2, allOrgs.size) + } + + @Test + fun `getEmail should return null when no email contact method`() { + val retrieved = organizationRepository.findById(ownerOrg.id!!).get() + val email = retrieved.getEmail() + + assertNull(email) + } + + @Test + fun `getMobile should return null when no mobile contact method`() { + val retrieved = organizationRepository.findById(ownerOrg.id!!).get() + val mobile = retrieved.getMobile() + + assertNull(mobile) + } +} diff --git a/src/test/kotlin/be/sgl/backend/service/user/UserServiceIntegrationTest.kt b/src/test/kotlin/be/sgl/backend/service/user/UserServiceIntegrationTest.kt new file mode 100644 index 0000000..dcd86f9 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/service/user/UserServiceIntegrationTest.kt @@ -0,0 +1,289 @@ +package be.sgl.backend.service.user + +import be.sgl.backend.config.TestConfigurations +import be.sgl.backend.entity.branch.Branch +import be.sgl.backend.entity.branch.BranchStatus +import be.sgl.backend.entity.user.Sex +import be.sgl.backend.entity.user.StaffData +import be.sgl.backend.entity.user.User +import be.sgl.backend.repository.BranchRepository +import be.sgl.backend.repository.user.UserRepository +import be.sgl.backend.service.exception.UserNotFoundException +import be.sgl.backend.util.IntegrationTest +import jakarta.transaction.Transactional +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Import +import java.time.LocalDate + +@IntegrationTest +@Import(TestConfigurations::class) +@Transactional +class UserServiceIntegrationTest { + + @Autowired + private lateinit var userService: UserService + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var branchRepository: BranchRepository + + private lateinit var testUser: User + private lateinit var testBranch: Branch + + @BeforeEach + fun setup() { + userRepository.deleteAll() + branchRepository.deleteAll() + + testBranch = Branch().apply { + name = "Test Branch" + email = "test@example.com" + minimumAge = 6 + maximumAge = 12 + sex = Sex.UNKNOWN + description = "Test" + law = "Law" + image = "test.jpg" + status = BranchStatus.ACTIVE + staffTitle = "Leader" + } + testBranch = branchRepository.save(testBranch) + + testUser = User().apply { + username = "testuser" + firstName = "Test" + name = "User" + email = "testuser@example.com" + birthdate = LocalDate.of(2000, 1, 1) + sex = Sex.MALE + mobile = "0123456789" + hasReduction = false + hasHandicap = false + } + testUser = userRepository.save(testUser) + } + + @Test + fun `getProfile should return user profile`() { + val profile = userService.getProfile(testUser.username!!) + + assertNotNull(profile) + assertEquals(testUser.username, profile.username) + assertEquals(testUser.email, profile.email) + assertEquals(testUser.firstName, profile.firstName) + assertEquals(testUser.name, profile.name) + } + + @Test + fun `getProfile should throw exception for non-existent user`() { + assertThrows(UserNotFoundException::class.java) { + userService.getProfile("nonexistent") + } + } + + @Test + fun `getByQuery should find users by name`() { + val user2 = User().apply { + username = "anotheruser" + firstName = "Another" + name = "User" + email = "another@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + userRepository.save(user2) + + val user3 = User().apply { + username = "different" + firstName = "Different" + name = "Person" + email = "different@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + userRepository.save(user3) + userRepository.flush() + + val results = userService.getByQuery("User") + + assertTrue(results.size >= 2) + assertTrue(results.any { it.name == "User" }) + } + + @Test + fun `getByQuery should find users by email`() { + val results = userService.getByQuery(testUser.email) + + assertTrue(results.any { it.email == testUser.email }) + } + + @Test + fun `getByQuery should find users by first name`() { + val results = userService.getByQuery(testUser.firstName) + + assertTrue(results.any { it.firstName == testUser.firstName }) + } + + @Test + fun `getByQuery should return empty list when no matches`() { + val results = userService.getByQuery("NonExistentQuery12345") + + assertTrue(results.isEmpty()) + } + + @Test + fun `getStaffBranch should return branch when user is staff`() { + testUser.staffData = StaffData().apply { + user = testUser + branch = testBranch + title = "Leader" + } + userRepository.save(testUser) + userRepository.flush() + + val staffBranch = userService.getStaffBranch(testUser.username!!) + + assertNotNull(staffBranch) + assertEquals(testBranch.name, staffBranch?.name) + } + + @Test + fun `getStaffBranch should return null when user is not staff`() { + val staffBranch = userService.getStaffBranch(testUser.username!!) + + assertNull(staffBranch) + } + + @Test + fun `user profile should include all personal information`() { + testUser.mobile = "0123456789" + testUser.nis = "12345678901" + testUser.accountNo = "BE12345678901234" + testUser.hasReduction = true + testUser.hasHandicap = false + userRepository.save(testUser) + userRepository.flush() + + val profile = userService.getProfile(testUser.username!!) + + assertEquals("0123456789", profile.mobile) + assertEquals("12345678901", profile.nis) + assertEquals("BE12345678901234", profile.accountNo) + assertTrue(profile.hasReduction) + assertFalse(profile.hasHandicap) + } + + @Test + fun `getByQuery should be case insensitive`() { + val results1 = userService.getByQuery("TEST") + val results2 = userService.getByQuery("test") + + assertTrue(results1.any { it.firstName.equals("Test", ignoreCase = true) }) + assertTrue(results2.any { it.firstName.equals("Test", ignoreCase = true) }) + } + + @Test + fun `user profile should include birthdate and sex`() { + val profile = userService.getProfile(testUser.username!!) + + assertEquals(LocalDate.of(2000, 1, 1), profile.birthdate) + assertEquals(Sex.MALE, profile.sex) + } + + @Test + fun `getByQuery should support partial matching`() { + val results = userService.getByQuery("Test") + + assertTrue(results.any { it.firstName.contains("Test", ignoreCase = true) }) + } + + @Test + fun `user with multiple properties should be searchable by any property`() { + val user = User().apply { + username = "searchable" + firstName = "Searchable" + name = "TestName" + email = "searchable@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + userRepository.save(user) + userRepository.flush() + + val byFirstName = userService.getByQuery("Searchable") + val byLastName = userService.getByQuery("TestName") + val byEmail = userService.getByQuery("searchable@") + + assertTrue(byFirstName.any { it.username == "searchable" }) + assertTrue(byLastName.any { it.username == "searchable" }) + assertTrue(byEmail.any { it.username == "searchable" }) + } + + @Test + fun `user profile should include age deviation`() { + testUser.ageDeviation = 2 + userRepository.save(testUser) + userRepository.flush() + + val profile = userService.getProfile(testUser.username!!) + + assertEquals(2, profile.ageDeviation) + } + + @Test + fun `user profile should handle null optional fields`() { + val minimalUser = User().apply { + username = "minimal" + firstName = "Minimal" + name = "User" + email = "minimal@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + userRepository.save(minimalUser) + userRepository.flush() + + val profile = userService.getProfile(minimalUser.username!!) + + assertNotNull(profile) + assertNull(profile.mobile) + assertNull(profile.nis) + assertNull(profile.accountNo) + assertNull(profile.image) + } + + @Test + fun `getStaffBranch should handle user with multiple branch associations`() { + // User should only have one staff branch at a time + testUser.staffData = StaffData().apply { + user = testUser + branch = testBranch + title = "Leader" + } + userRepository.save(testUser) + userRepository.flush() + + val staffBranch = userService.getStaffBranch(testUser.username!!) + + assertNotNull(staffBranch) + assertEquals(testBranch.id, staffBranch?.id) + } + + @Test + fun `user search should handle special characters`() { + val userWithSpecialChars = User().apply { + username = "special" + firstName = "Jean-Pierre" + name = "D'Angelo" + email = "special@example.com" + birthdate = LocalDate.of(2000, 1, 1) + } + userRepository.save(userWithSpecialChars) + userRepository.flush() + + val results = userService.getByQuery("Jean") + + assertTrue(results.any { it.firstName == "Jean-Pierre" }) + } +} diff --git a/src/test/kotlin/be/sgl/backend/util/ConditionalsTest.kt b/src/test/kotlin/be/sgl/backend/util/ConditionalsTest.kt new file mode 100644 index 0000000..6095c87 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/util/ConditionalsTest.kt @@ -0,0 +1,128 @@ +package be.sgl.backend.util + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.mockito.Mockito.* +import org.springframework.context.annotation.ConditionContext +import org.springframework.core.env.Environment +import org.springframework.core.type.AnnotatedTypeMetadata + +class ConditionalsTest { + + @Test + fun `WhenNotBlankCondition should match when property is not blank and not is false`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + val environment = mock(Environment::class.java) + + `when`(context.environment).thenReturn(environment) + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)) + .thenReturn(mapOf("value" to "test.property", "not" to false)) + `when`(environment.getProperty("test.property")).thenReturn("some-value") + + assertTrue(condition.matches(context, metadata)) + } + + @Test + fun `WhenNotBlankCondition should not match when property is blank and not is false`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + val environment = mock(Environment::class.java) + + `when`(context.environment).thenReturn(environment) + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)) + .thenReturn(mapOf("value" to "test.property", "not" to false)) + `when`(environment.getProperty("test.property")).thenReturn("") + + assertFalse(condition.matches(context, metadata)) + } + + @Test + fun `WhenNotBlankCondition should match when property is blank and not is true`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + val environment = mock(Environment::class.java) + + `when`(context.environment).thenReturn(environment) + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)) + .thenReturn(mapOf("value" to "test.property", "not" to true)) + `when`(environment.getProperty("test.property")).thenReturn("") + + assertTrue(condition.matches(context, metadata)) + } + + @Test + fun `WhenNotBlankCondition should not match when property is not blank and not is true`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + val environment = mock(Environment::class.java) + + `when`(context.environment).thenReturn(environment) + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)) + .thenReturn(mapOf("value" to "test.property", "not" to true)) + `when`(environment.getProperty("test.property")).thenReturn("some-value") + + assertFalse(condition.matches(context, metadata)) + } + + @Test + fun `WhenNotBlankCondition should not match when property is null and not is false`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + val environment = mock(Environment::class.java) + + `when`(context.environment).thenReturn(environment) + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)) + .thenReturn(mapOf("value" to "test.property", "not" to false)) + `when`(environment.getProperty("test.property")).thenReturn(null) + + assertFalse(condition.matches(context, metadata)) + } + + @Test + fun `WhenNotBlankCondition should match when property is null and not is true`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + val environment = mock(Environment::class.java) + + `when`(context.environment).thenReturn(environment) + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)) + .thenReturn(mapOf("value" to "test.property", "not" to true)) + `when`(environment.getProperty("test.property")).thenReturn(null) + + assertTrue(condition.matches(context, metadata)) + } + + @Test + fun `WhenNotBlankCondition should return false when annotation attributes are null`() { + val condition = WhenNotBlankCondition() + val context = mock(ConditionContext::class.java) + val metadata = mock(AnnotatedTypeMetadata::class.java) + + `when`(metadata.getAnnotationAttributes(WhenNotBlank::class.java.name)).thenReturn(null) + + assertFalse(condition.matches(context, metadata)) + } + + @Test + fun `ForInternalOrganization annotation should be present`() { + val annotation = ForInternalOrganization::class.java.getAnnotation(WhenNotBlank::class.java) + assertNotNull(annotation) + assertEquals("organization.external.id", annotation.value) + assertTrue(annotation.not) + } + + @Test + fun `ForExternalOrganization annotation should be present`() { + val annotation = ForExternalOrganization::class.java.getAnnotation(WhenNotBlank::class.java) + assertNotNull(annotation) + assertEquals("organization.external.id", annotation.value) + assertFalse(annotation.not) + } +} diff --git a/src/test/kotlin/be/sgl/backend/util/TextUtilsTest.kt b/src/test/kotlin/be/sgl/backend/util/TextUtilsTest.kt new file mode 100644 index 0000000..172ef98 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/util/TextUtilsTest.kt @@ -0,0 +1,75 @@ +package be.sgl.backend.util + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.Base64 + +class TextUtilsTest { + + @Test + fun `nullIfBlank should return null for blank strings`() { + assertNull("".nullIfBlank()) + assertNull(" ".nullIfBlank()) + assertNull(null.nullIfBlank()) + } + + @Test + fun `nullIfBlank should return value for non-blank strings`() { + assertEquals("test", "test".nullIfBlank()) + assertEquals("hello world", "hello world".nullIfBlank()) + } + + @Test + fun `base64Encoded should encode string correctly`() { + val input = "Hello World" + val expected = Base64.getEncoder().encodeToString(input.toByteArray()) + assertEquals(expected, input.base64Encoded()) + } + + @Test + fun `base64Encoded should handle empty string`() { + val input = "" + val expected = Base64.getEncoder().encodeToString(input.toByteArray()) + assertEquals(expected, input.base64Encoded()) + } + + @Test + fun `belgian date format for LocalDate should be dd-MM-yyyy`() { + val date = LocalDate.of(2023, 12, 25) + assertEquals("25/12/2023", date.belgian()) + } + + @Test + fun `belgian date format for LocalDateTime should be dd-MM-yyyy`() { + val dateTime = LocalDateTime.of(2023, 12, 25, 15, 30, 45) + assertEquals("25/12/2023", dateTime.belgian()) + } + + @Test + fun `pricePrecision should format double with 2 decimals`() { + assertEquals("10.50", 10.5.pricePrecision()) + assertEquals("10.99", 10.99.pricePrecision()) + assertEquals("10.00", 10.0.pricePrecision()) + } + + @Test + fun `pricePrecision should return null for null input`() { + val value: Double? = null + assertNull(value.pricePrecision()) + } + + @Test + fun `reducePrice should calculate reduced price correctly`() { + assertEquals(50.0, 100.0.reducePrice(2.0)) + assertEquals(33.33, 100.0.reducePrice(3.0)) + assertEquals(25.0, 50.0.reducePrice(2.0)) + } + + @Test + fun `reducePrice should round to 2 decimal places`() { + val result = 10.0.reducePrice(3.0) + assertEquals(3.33, result) + } +} diff --git a/src/test/kotlin/be/sgl/backend/util/UrlUtilsTest.kt b/src/test/kotlin/be/sgl/backend/util/UrlUtilsTest.kt new file mode 100644 index 0000000..3f9dc0e --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/util/UrlUtilsTest.kt @@ -0,0 +1,90 @@ +package be.sgl.backend.util + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class UrlUtilsTest { + + @Test + fun `appendRequestParameters should add single parameter to URL without query string`() { + val result = appendRequestParameters("https://example.com/api", "key" to "value") + assertEquals("https://example.com/api?key=value", result) + } + + @Test + fun `appendRequestParameters should add multiple parameters to URL without query string`() { + val result = appendRequestParameters( + "https://example.com/api", + "key1" to "value1", + "key2" to "value2" + ) + assertEquals("https://example.com/api?key1=value1&key2=value2", result) + } + + @Test + fun `appendRequestParameters should append to existing query string`() { + val result = appendRequestParameters( + "https://example.com/api?existing=param", + "key" to "value" + ) + assertEquals("https://example.com/api?existing=param&key=value", result) + } + + @Test + fun `appendRequestParameters should URL encode parameter names`() { + val result = appendRequestParameters( + "https://example.com/api", + "key with spaces" to "value" + ) + assertTrue(result.contains("key+with+spaces=value")) + } + + @Test + fun `appendRequestParameters should URL encode parameter values`() { + val result = appendRequestParameters( + "https://example.com/api", + "key" to "value with spaces" + ) + assertTrue(result.contains("key=value+with+spaces")) + } + + @Test + fun `appendRequestParameters should handle special characters`() { + val result = appendRequestParameters( + "https://example.com/api", + "email" to "test@example.com" + ) + assertTrue(result.contains("email=test%40example.com")) + } + + @Test + fun `appendRequestParameters should handle null values`() { + val result = appendRequestParameters( + "https://example.com/api", + "key" to null + ) + assertEquals("https://example.com/api?key=null", result) + } + + @Test + fun `appendRequestParameters should handle numeric values`() { + val result = appendRequestParameters( + "https://example.com/api", + "count" to 42, + "price" to 19.99 + ) + assertTrue(result.contains("count=42")) + assertTrue(result.contains("price=19.99")) + } + + @Test + fun `appendRequestParameters should handle boolean values`() { + val result = appendRequestParameters( + "https://example.com/api", + "active" to true, + "deleted" to false + ) + assertTrue(result.contains("active=true")) + assertTrue(result.contains("deleted=false")) + } +} diff --git a/src/test/kotlin/be/sgl/backend/util/ValidationUtilsTest.kt b/src/test/kotlin/be/sgl/backend/util/ValidationUtilsTest.kt new file mode 100644 index 0000000..b8aa022 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/util/ValidationUtilsTest.kt @@ -0,0 +1,226 @@ +package be.sgl.backend.util + +import jakarta.validation.Validation +import jakarta.validation.Validator +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.time.LocalDateTime + +class ValidationUtilsTest { + + private lateinit var validator: Validator + + @BeforeEach + fun setup() { + validator = Validation.buildDefaultValidatorFactory().validator + } + + // PhoneNumber validation tests + @Test + fun `PhoneNumberValidator should accept valid 9 digit phone numbers`() { + val phoneValidator = PhoneNumberValidator() + assertTrue(phoneValidator.isValid("123456789", null)) + assertTrue(phoneValidator.isValid("987654321", null)) + } + + @Test + fun `PhoneNumberValidator should accept valid 10 digit phone numbers`() { + val phoneValidator = PhoneNumberValidator() + assertTrue(phoneValidator.isValid("0123456789", null)) + assertTrue(phoneValidator.isValid("0987654321", null)) + } + + @Test + fun `PhoneNumberValidator should reject invalid phone numbers`() { + val phoneValidator = PhoneNumberValidator() + assertFalse(phoneValidator.isValid("12345", null)) + assertFalse(phoneValidator.isValid("12345678901", null)) + assertFalse(phoneValidator.isValid("abcdefghi", null)) + assertFalse(phoneValidator.isValid("123-456-789", null)) + } + + @Test + fun `PhoneNumberValidator should accept null or blank values`() { + val phoneValidator = PhoneNumberValidator() + assertTrue(phoneValidator.isValid(null, null)) + assertTrue(phoneValidator.isValid("", null)) + assertTrue(phoneValidator.isValid(" ", null)) + } + + // NIS validation tests + @Test + fun `NisValidator should accept valid NIS numbers`() { + val nisValidator = NisValidator() + // Valid NIS: 85073003328 (example) + assertTrue(nisValidator.isValid("85073003328", null)) + } + + @Test + fun `NisValidator should reject invalid NIS numbers`() { + val nisValidator = NisValidator() + assertFalse(nisValidator.isValid("12345678901", null)) + assertFalse(nisValidator.isValid("00000000000", null)) + } + + @Test + fun `NisValidator should reject NIS with wrong length`() { + val nisValidator = NisValidator() + assertFalse(nisValidator.isValid("123456789", null)) + assertFalse(nisValidator.isValid("123456789012", null)) + } + + @Test + fun `NisValidator should reject non-numeric NIS`() { + val nisValidator = NisValidator() + assertFalse(nisValidator.isValid("abcdefghijk", null)) + assertFalse(nisValidator.isValid("123-456-789", null)) + } + + @Test + fun `NisValidator should accept null values`() { + val nisValidator = NisValidator() + assertTrue(nisValidator.isValid(null, null)) + } + + // CountryCode validation tests + @Test + fun `CountryCodeValidator should accept valid ISO country codes`() { + val countryValidator = CountryCodeValidator() + assertTrue(countryValidator.isValid("BE", null)) + assertTrue(countryValidator.isValid("NL", null)) + assertTrue(countryValidator.isValid("FR", null)) + assertTrue(countryValidator.isValid("US", null)) + assertTrue(countryValidator.isValid("be", null)) // lowercase should be accepted + } + + @Test + fun `CountryCodeValidator should reject invalid country codes`() { + val countryValidator = CountryCodeValidator() + assertFalse(countryValidator.isValid("XX", null)) + assertFalse(countryValidator.isValid("ZZZ", null)) + assertFalse(countryValidator.isValid("123", null)) + } + + @Test + fun `CountryCodeValidator should accept null values`() { + val countryValidator = CountryCodeValidator() + assertTrue(countryValidator.isValid(null, null)) + } + + // KBO validation tests + @Test + fun `KboValidator should accept valid KBO numbers`() { + val kboValidator = KboValidator() + assertTrue(kboValidator.isValid("0123456789", null)) + assertTrue(kboValidator.isValid("0987654321", null)) + } + + @Test + fun `KboValidator should reject KBO numbers not starting with 0`() { + val kboValidator = KboValidator() + assertFalse(kboValidator.isValid("1123456789", null)) + assertFalse(kboValidator.isValid("9987654321", null)) + } + + @Test + fun `KboValidator should reject invalid KBO format`() { + val kboValidator = KboValidator() + assertFalse(kboValidator.isValid("012345678", null)) // too short + assertFalse(kboValidator.isValid("01234567890", null)) // too long + assertFalse(kboValidator.isValid("012345678a", null)) // contains letter + } + + @Test + fun `KboValidator should accept null or blank values`() { + val kboValidator = KboValidator() + assertTrue(kboValidator.isValid(null, null)) + assertTrue(kboValidator.isValid("", null)) + assertTrue(kboValidator.isValid(" ", null)) + } + + // StartEndTime validation tests + @Test + fun `StartEndTimeValidator should accept valid time ranges`() { + val validator = StartEndTimeValidator() + val validRange = object : StartEndTime { + override val start = LocalDateTime.of(2023, 1, 1, 10, 0) + override val end = LocalDateTime.of(2023, 1, 1, 12, 0) + } + assertTrue(validator.isValid(validRange, null)) + } + + @Test + fun `StartEndTimeValidator should reject end before start`() { + val validator = StartEndTimeValidator() + val invalidRange = object : StartEndTime { + override val start = LocalDateTime.of(2023, 1, 1, 12, 0) + override val end = LocalDateTime.of(2023, 1, 1, 10, 0) + } + assertFalse(validator.isValid(invalidRange, null)) + } + + @Test + fun `StartEndTimeValidator should accept null values`() { + val validator = StartEndTimeValidator() + assertTrue(validator.isValid(null, null)) + } + + @Test + fun `StartEndTimeValidator should accept null start or end`() { + val validator = StartEndTimeValidator() + val nullStart = object : StartEndTime { + override val start: LocalDateTime? = null + override val end = LocalDateTime.of(2023, 1, 1, 12, 0) + } + val nullEnd = object : StartEndTime { + override val start = LocalDateTime.of(2023, 1, 1, 10, 0) + override val end: LocalDateTime? = null + } + assertTrue(validator.isValid(nullStart, null)) + assertTrue(validator.isValid(nullEnd, null)) + } + + // StartEndDate validation tests + @Test + fun `StartEndDateValidator should accept valid date ranges`() { + val validator = StartEndDateValidator() + val validRange = object : StartEndDate { + override val start = LocalDate.of(2023, 1, 1) + override val end = LocalDate.of(2023, 12, 31) + } + assertTrue(validator.isValid(validRange, null)) + } + + @Test + fun `StartEndDateValidator should reject end before start`() { + val validator = StartEndDateValidator() + val invalidRange = object : StartEndDate { + override val start = LocalDate.of(2023, 12, 31) + override val end = LocalDate.of(2023, 1, 1) + } + assertFalse(validator.isValid(invalidRange, null)) + } + + @Test + fun `StartEndDateValidator should accept null values`() { + val validator = StartEndDateValidator() + assertTrue(validator.isValid(null, null)) + } + + @Test + fun `StartEndDateValidator should accept null start or end`() { + val validator = StartEndDateValidator() + val nullStart = object : StartEndDate { + override val start: LocalDate? = null + override val end = LocalDate.of(2023, 12, 31) + } + val nullEnd = object : StartEndDate { + override val start = LocalDate.of(2023, 1, 1) + override val end: LocalDate? = null + } + assertTrue(validator.isValid(nullStart, null)) + assertTrue(validator.isValid(nullEnd, null)) + } +} diff --git a/src/test/kotlin/be/sgl/backend/util/ZipUtilsTest.kt b/src/test/kotlin/be/sgl/backend/util/ZipUtilsTest.kt new file mode 100644 index 0000000..125da67 --- /dev/null +++ b/src/test/kotlin/be/sgl/backend/util/ZipUtilsTest.kt @@ -0,0 +1,110 @@ +package be.sgl.backend.util + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.util.zip.ZipInputStream + +class ZipUtilsTest { + + @Test + fun `zipped should create a zip file from list of byte arrays`() { + val file1 = "Content of file 1".toByteArray() + val file2 = "Content of file 2".toByteArray() + val files = listOf(file1, file2) + + val zipped = files.zipped() + + assertNotNull(zipped) + assertTrue(zipped.isNotEmpty()) + } + + @Test + fun `zipped should contain correct number of entries`() { + val file1 = "Content of file 1".toByteArray() + val file2 = "Content of file 2".toByteArray() + val file3 = "Content of file 3".toByteArray() + val files = listOf(file1, file2, file3) + + val zipped = files.zipped() + + val zipInputStream = ZipInputStream(ByteArrayInputStream(zipped)) + var entryCount = 0 + while (zipInputStream.nextEntry != null) { + entryCount++ + } + + assertEquals(3, entryCount) + } + + @Test + fun `zipped should name files correctly`() { + val file1 = "Content of file 1".toByteArray() + val file2 = "Content of file 2".toByteArray() + val files = listOf(file1, file2) + + val zipped = files.zipped() + + val zipInputStream = ZipInputStream(ByteArrayInputStream(zipped)) + val entry1 = zipInputStream.nextEntry + val entry2 = zipInputStream.nextEntry + + assertEquals("file0.pdf", entry1.name) + assertEquals("file1.pdf", entry2.name) + } + + @Test + fun `zipped should preserve content of files`() { + val content1 = "Content of file 1".toByteArray() + val content2 = "Content of file 2".toByteArray() + val files = listOf(content1, content2) + + val zipped = files.zipped() + + val zipInputStream = ZipInputStream(ByteArrayInputStream(zipped)) + zipInputStream.nextEntry + val extractedContent1 = zipInputStream.readAllBytes() + zipInputStream.nextEntry + val extractedContent2 = zipInputStream.readAllBytes() + + assertArrayEquals(content1, extractedContent1) + assertArrayEquals(content2, extractedContent2) + } + + @Test + fun `zipped should handle empty list`() { + val files = emptyList() + + val zipped = files.zipped() + + assertNotNull(zipped) + assertTrue(zipped.isNotEmpty()) + } + + @Test + fun `zipped should handle single file`() { + val file = "Single file content".toByteArray() + val files = listOf(file) + + val zipped = files.zipped() + + val zipInputStream = ZipInputStream(ByteArrayInputStream(zipped)) + var entryCount = 0 + while (zipInputStream.nextEntry != null) { + entryCount++ + } + + assertEquals(1, entryCount) + } + + @Test + fun `zipped should handle large files`() { + val largeContent = ByteArray(10000) { it.toByte() } + val files = listOf(largeContent) + + val zipped = files.zipped() + + assertNotNull(zipped) + assertTrue(zipped.isNotEmpty()) + } +}