diff --git a/auditlog/pom.xml b/auditlog/pom.xml index e921f0e8..7df8c712 100644 --- a/auditlog/pom.xml +++ b/auditlog/pom.xml @@ -71,6 +71,12 @@ h2 runtime + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + diff --git a/auditlog/src/main/resources/application.properties b/auditlog/src/main/resources/application.properties deleted file mode 100644 index d4d640e5..00000000 --- a/auditlog/src/main/resources/application.properties +++ /dev/null @@ -1,4 +0,0 @@ -spring.datasource.url=${SPRING_DATASOURCE_URL} -spring.datasource.username=${SPRING_DATASOURCE_USERNAME} -spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} - diff --git a/auditlog/src/main/resources/application.yml b/auditlog/src/main/resources/application.yml index 24cd7618..97d6d399 100644 --- a/auditlog/src/main/resources/application.yml +++ b/auditlog/src/main/resources/application.yml @@ -1,17 +1,24 @@ server: port: 8089 spring: - application: - name: auditlog-service - rabbitmq: - addresses: rabbitmq:5672 - username: shiftcontrol - password: password - jpa: - properties: - hibernate: - type: - json_format_mapper: jsonb + application: + name: auditlog-service + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + mvc: + servlet: + path: ${AUDITLOG_REST_PATH_PREFIX:/} + rabbitmq: + addresses: rabbitmq:5672 + username: shiftcontrol + password: password + jpa: + properties: + hibernate: + type: + json_format_mapper: jsonb keycloak: auth-server-url: http://keycloak.127.0.0.1.nip.io/ auth-realm: dev diff --git a/auditlog/src/test/java/at/shiftcontrol/auditlog/endpoint/LogEndpointPathPrefixTest.java b/auditlog/src/test/java/at/shiftcontrol/auditlog/endpoint/LogEndpointPathPrefixTest.java new file mode 100644 index 00000000..cd7a6f36 --- /dev/null +++ b/auditlog/src/test/java/at/shiftcontrol/auditlog/endpoint/LogEndpointPathPrefixTest.java @@ -0,0 +1,71 @@ +package at.shiftcontrol.auditlog.endpoint; + +import java.time.Instant; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.Test; + +import at.shiftcontrol.auditlog.dto.LogEntryDto; +import at.shiftcontrol.auditlog.dto.LogSearchDto; +import at.shiftcontrol.auditlog.service.AuditLogService; +import at.shiftcontrol.lib.dto.PaginationDto; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest( + controllers = LogEndpoint.class, + properties = "spring.mvc.servlet.path=/auditlog", + excludeAutoConfiguration = OAuth2ResourceServerAutoConfiguration.class +) +@AutoConfigureMockMvc(addFilters = false) +@Import(LogEndpointPathPrefixTest.TestConfig.class) +class LogEndpointPathPrefixTest { + + @Autowired + private MockMvc mockMvc; + + @Value("${spring.mvc.servlet.path}") + private String servletPath; + + @Test + void shouldExposeEndpointUnderConfiguredPrefix() throws Exception { + org.junit.jupiter.api.Assertions.assertEquals("/auditlog", servletPath); + + mockMvc.perform(get("/auditlog/api/v1/log") + .servletPath("/auditlog") + .param("page", "0") + .param("size", "10") + .param("startTime", Instant.parse("2026-01-01T00:00:00Z").toString()) + .param("endTime", Instant.parse("2026-01-02T00:00:00Z").toString())) + .andExpect(status().isOk()); + } + + @TestConfiguration + static class TestConfig { + @Bean + @Primary + AuditLogService auditLogService() { + return new AuditLogService(null) { + @Override + public PaginationDto search(int page, int size, LogSearchDto searchDto) { + return PaginationDto.builder() + .page(page) + .pages(0) + .total(0) + .items(List.of()) + .build(); + } + }; + } + } +} diff --git a/auditlog/src/test/java/at/shiftcontrol/auditlog/endpoint/LogEndpointRootServletPathTest.java b/auditlog/src/test/java/at/shiftcontrol/auditlog/endpoint/LogEndpointRootServletPathTest.java new file mode 100644 index 00000000..bd3f9ecd --- /dev/null +++ b/auditlog/src/test/java/at/shiftcontrol/auditlog/endpoint/LogEndpointRootServletPathTest.java @@ -0,0 +1,70 @@ +package at.shiftcontrol.auditlog.endpoint; + +import java.time.Instant; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.Test; + +import at.shiftcontrol.auditlog.dto.LogEntryDto; +import at.shiftcontrol.auditlog.dto.LogSearchDto; +import at.shiftcontrol.auditlog.service.AuditLogService; +import at.shiftcontrol.lib.dto.PaginationDto; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest( + controllers = LogEndpoint.class, + properties = "spring.mvc.servlet.path=/", + excludeAutoConfiguration = OAuth2ResourceServerAutoConfiguration.class +) +@AutoConfigureMockMvc(addFilters = false) +@Import(LogEndpointRootServletPathTest.TestConfig.class) +class LogEndpointRootServletPathTest { + + @Autowired + private MockMvc mockMvc; + + @Value("${spring.mvc.servlet.path}") + private String servletPath; + + @Test + void shouldExposeEndpointWithoutAdditionalPrefix() throws Exception { + org.junit.jupiter.api.Assertions.assertEquals("/", servletPath); + + mockMvc.perform(get("/api/v1/log") + .param("page", "0") + .param("size", "10") + .param("startTime", Instant.parse("2026-01-01T00:00:00Z").toString()) + .param("endTime", Instant.parse("2026-01-02T00:00:00Z").toString())) + .andExpect(status().isOk()); + } + + @TestConfiguration + static class TestConfig { + @Bean + @Primary + AuditLogService auditLogService() { + return new AuditLogService(null) { + @Override + public PaginationDto search(int page, int size, LogSearchDto searchDto) { + return PaginationDto.builder() + .page(page) + .pages(0) + .total(0) + .items(List.of()) + .build(); + } + }; + } + } +} diff --git a/shiftservice/README.md b/shiftservice/README.md new file mode 100644 index 00000000..1f0fbd80 --- /dev/null +++ b/shiftservice/README.md @@ -0,0 +1,65 @@ +# Shiftservice + +## Runtime configuration + +`shiftservice` expects these environment variables in normal runtime: + +- `SPRING_DATASOURCE_URL` +- `SPRING_DATASOURCE_USERNAME` +- `SPRING_DATASOURCE_PASSWORD` + +Optional service-specific variables: + +- `SHIFTSERVICE_REST_PATH_PREFIX` + - Defaults to `/`, which keeps endpoints at `/api/v1/...` with no extra prefix. + - Example: set `/shiftservice` to expose endpoints as `/shiftservice/api/v1/...`. +- `SPRING_LIQUIBASE_CONTEXTS` + - Defaults to `dev`. + - Controls which Liquibase context-tagged change sets are applied. + +## Database and Liquibase + +Main configuration lives in [application.yml](./src/main/resources/application.yml). + +The service runs Hibernate with `ddl-auto=validate`, so schema creation and migration are handled by Liquibase, not Hibernate. + +Liquibase is enabled by default and uses: + +- changelog: `classpath:db/changelog-master.xml` +- default context: `dev` + +## Environments + +### Development + +Use: + +- `SPRING_LIQUIBASE_CONTEXTS=dev` or leave it unset + +Effect: + +- schema migrations run +- development seed data in `db/data/*` runs because those change sets are tagged with `context="dev"` + +### Production + +Use: + +- `SPRING_LIQUIBASE_CONTEXTS=prod` + +Effect: + +- schema migrations still run +- `dev` seed data is skipped + +This matters because the seed data files under `db/data/` are marked with the `dev` Liquibase context. + +## OpenAPI profile + +The `openapi` profile uses [application-openapi.yml](./src/main/resources/application-openapi.yml): + +- in-memory H2 datasource +- Liquibase disabled +- Hibernate `ddl-auto=none` + +That profile is intended only for OpenAPI generation, not normal app runtime. diff --git a/shiftservice/pom.xml b/shiftservice/pom.xml index d2faa226..e52e7120 100644 --- a/shiftservice/pom.xml +++ b/shiftservice/pom.xml @@ -90,6 +90,11 @@ spring-security-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + org.apache.poi poi-ooxml @@ -122,6 +127,12 @@ + + unit-tests + + true + + openapi diff --git a/shiftservice/src/main/resources/application.properties b/shiftservice/src/main/resources/application.properties deleted file mode 100644 index 64cbaa11..00000000 --- a/shiftservice/src/main/resources/application.properties +++ /dev/null @@ -1,12 +0,0 @@ -spring.datasource.url=${SPRING_DATASOURCE_URL} -spring.datasource.username=${SPRING_DATASOURCE_USERNAME} -spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} -# let liquibase handle table creation -spring.jpa.hibernate.ddl-auto=validate -spring.jpa.database=postgresql -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.liquibase.change-log=classpath:db/changelog-master.xml -spring.liquibase.enabled=true -# set context to dev to insert test-data, else use prod -spring.liquibase.contexts=dev - diff --git a/shiftservice/src/main/resources/application.yml b/shiftservice/src/main/resources/application.yml index 4aaa4b33..d5f72ce6 100644 --- a/shiftservice/src/main/resources/application.yml +++ b/shiftservice/src/main/resources/application.yml @@ -1,15 +1,31 @@ spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} rabbitmq: addresses: rabbitmq:5672 username: shiftcontrol password: password jackson: default-property-inclusion: non_null + mvc: + servlet: + path: ${SHIFTSERVICE_REST_PATH_PREFIX:/} + jpa: + database: postgresql + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: validate security: oauth2: resourceserver: jwt: issuer-uri: http://keycloak.127.0.0.1.nip.io/realms/dev + liquibase: + change-log: classpath:db/changelog-master.xml + enabled: true + contexts: ${SPRING_LIQUIBASE_CONTEXTS:dev} keycloak: auth-server-url: http://keycloak.127.0.0.1.nip.io/ auth-realm: dev diff --git a/shiftservice/src/test/java/at/shiftcontrol/shiftservice/endpoint/UserProfileEndpointRootServletPathTest.java b/shiftservice/src/test/java/at/shiftcontrol/shiftservice/endpoint/UserProfileEndpointRootServletPathTest.java new file mode 100644 index 00000000..b2dfdf38 --- /dev/null +++ b/shiftservice/src/test/java/at/shiftcontrol/shiftservice/endpoint/UserProfileEndpointRootServletPathTest.java @@ -0,0 +1,114 @@ +package at.shiftcontrol.shiftservice.endpoint; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import at.shiftcontrol.shiftservice.auth.ApplicationUserProvider; +import at.shiftcontrol.shiftservice.auth.user.ShiftControlUser; +import at.shiftcontrol.shiftservice.dto.userprofile.NotificationSettingsDto; +import at.shiftcontrol.shiftservice.dto.userprofile.UserProfileDto; +import at.shiftcontrol.shiftservice.service.userprofile.NotificationService; +import at.shiftcontrol.shiftservice.service.userprofile.UserProfileService; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest( + controllers = UserProfileEndpoint.class, + properties = "spring.mvc.servlet.path=/", + excludeAutoConfiguration = OAuth2ResourceServerAutoConfiguration.class, + excludeFilters = @ComponentScan.Filter( + type = FilterType.REGEX, + pattern = "at\\.shiftcontrol\\.shiftservice\\.config\\.TraceIdFilter" + ) +) +@AutoConfigureMockMvc(addFilters = false) +@Import(UserProfileEndpointRootServletPathTest.TestConfig.class) +class UserProfileEndpointRootServletPathTest { + + @Autowired + private MockMvc mockMvc; + + @Value("${spring.mvc.servlet.path}") + private String servletPath; + + @Test + void shouldExposeEndpointWithoutAdditionalPrefix() throws Exception { + Assertions.assertEquals("/", servletPath); + + mockMvc.perform(get("/api/v1/me/profile")) + .andExpect(status().isOk()); + } + + @TestConfiguration + static class TestConfig { + @Bean + @Primary + ApplicationUserProvider applicationUserProvider() { + return new ApplicationUserProvider() { + @Override + public ShiftControlUser getCurrentUser() { + return new ShiftControlUser(List.of(), "test-user", "123456789") { + @Override + public boolean isVolunteerInPlan(long shiftPlanId) { + return false; + } + + @Override + public boolean isPlannerInPlan(long shiftPlanId) { + return false; + } + + @Override + public boolean isLockedInPlan(long shiftPlanId) { + return false; + } + }; + } + }; + } + + @Bean + @Primary + UserProfileService userProfileService() { + return userId -> UserProfileDto.builder() + .account(null) + .notifications(List.of()) + .assignedRoles(List.of()) + .planningPlans(List.of()) + .volunteeringPlans(List.of()) + .planningEvents(List.of()) + .volunteeringEvents(List.of()) + .build(); + } + + @Bean + @Primary + NotificationService notificationService() { + return new NotificationService() { + @Override + public java.util.Collection getNotificationsForUser(String userId) { + return List.of(); + } + + @Override + public NotificationSettingsDto updateNotificationSetting(String userId, NotificationSettingsDto settingsDto) { + return settingsDto; + } + }; + } + } +} diff --git a/trustservice/pom.xml b/trustservice/pom.xml index 6850d496..0e6a8f07 100644 --- a/trustservice/pom.xml +++ b/trustservice/pom.xml @@ -84,4 +84,13 @@ + + + unit-tests + + true + + + +