From 7e1a2278fd7de03098a94f32f1e82a9d647b5fe1 Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sat, 9 May 2026 17:26:57 +0200 Subject: [PATCH 01/12] feat(SKY-101): add Rules GET API, entity, repository, use-case, controller, tests, and Flyway migration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mvnw | 9 ++ .../application/adapter/RuleMapper.java | 21 +++++ .../application/rules/GetRulesUseCase.java | 22 +++++ .../skyroster_backend/domain/model/Rule.java | 48 +++++++++++ .../domain/port/RuleRepository.java | 11 +++ .../adapter/in/web/RulesController.java | 72 ++++++++++++++++ .../out/persistence/JpaRuleRepository.java | 17 ++++ .../persistence/RuleRepositoryAdapter.java | 33 ++++++++ .../src/main/resources/api/openapi.yaml | 83 ++++++++++++++++++- .../V20260509__create_rules_table.sql | 15 ++++ .../rules/GetRulesUseCaseTest.java | 25 ++++++ .../integration/RulesControllerIT.java | 13 +++ 12 files changed, 368 insertions(+), 1 deletion(-) create mode 100755 mvnw create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java create mode 100644 skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..3545450 --- /dev/null +++ b/mvnw @@ -0,0 +1,9 @@ +#!/bin/sh +# Lightweight mvnw shim: delegate to system mvn if available, otherwise instruct user. +if command -v mvn >/dev/null 2>&1; then + mvn "$@" +else + echo "Maven not found. Please install Maven or add the official Maven Wrapper (mvnw)." + echo "You can also run: curl -o mvnw https://raw.githubusercontent.com/takari/maven-wrapper/master/mvnw && chmod +x mvnw" + exit 1 +fi diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java new file mode 100644 index 0000000..6a54a68 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/adapter/RuleMapper.java @@ -0,0 +1,21 @@ +package pl.skyroster.skyroster_backend.application.adapter; + +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.generated.model.RuleDto; + +public class RuleMapper { + public static RuleDto toDto(Rule r) { + RuleDto dto = new RuleDto(); + if (r.getId() != null) dto.setId(r.getId()); + dto.setName(r.getName()); + dto.setDescription(r.getDescription()); + dto.setRuleType(r.getRuleType()); + dto.setMaxMonthlyHours(r.getMaxMonthlyHours()); + dto.setMinRestMinutes(r.getMinRestMinutes()); + dto.setMinFlights(r.getMinFlights()); + dto.setActive(r.isActive()); + if (r.getCreatedAt() != null) dto.setCreatedAt(r.getCreatedAt()); + if (r.getUpdatedAt() != null) dto.setUpdatedAt(r.getUpdatedAt()); + return dto; + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java new file mode 100644 index 0000000..370552c --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCase.java @@ -0,0 +1,22 @@ +package pl.skyroster.skyroster_backend.application.rules; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import pl.skyroster.skyroster_backend.application.adapter.RuleMapper; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +@Service +public class GetRulesUseCase { + + private final RuleRepository ruleRepository; + + public GetRulesUseCase(RuleRepository ruleRepository) { + this.ruleRepository = ruleRepository; + } + + public Page execute(java.util.Optional type, java.util.Optional active, Pageable pageable) { + Page page = ruleRepository.findAllByTypeAndActive(type, active, pageable); + return page.map(RuleMapper::toDto); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java new file mode 100644 index 0000000..b38d4b3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java @@ -0,0 +1,48 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "rules") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Rule { + + @Id + private UUID id; + + @Column(nullable = false) + private String name; + + @Column(columnDefinition = "text") + private String description; + + @Column(name = "rule_type") + private String ruleType; + + @Column(name = "max_monthly_hours") + private Integer maxMonthlyHours; + + @Column(name = "min_rest_minutes") + private Integer minRestMinutes; + + @Column(name = "min_flights") + private Integer minFlights; + + @Column(nullable = false) + private boolean active = true; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java new file mode 100644 index 0000000..ab3d0d7 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java @@ -0,0 +1,11 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import pl.skyroster.skyroster_backend.domain.model.Rule; + +import java.util.Optional; + +public interface RuleRepository { + Page findAllByTypeAndActive(Optional type, Optional active, Pageable pageable); +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java new file mode 100644 index 0000000..74464af --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RulesController.java @@ -0,0 +1,72 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.rules.GetRulesUseCase; + +import java.util.ArrayList; +import java.util.List; + +@RestController +public class RulesController { + + private final GetRulesUseCase getRulesUseCase; + + public RulesController(GetRulesUseCase getRulesUseCase) { + this.getRulesUseCase = getRulesUseCase; + } + + @GetMapping("/api/v1/rules") + @PreAuthorize("hasRole('compliance_officer')") + public ResponseEntity getRules( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @RequestParam(required = false) String sort, + @RequestParam(required = false) String type, + @RequestParam(required = false) Boolean active + ) { + Sort sortObj = Sort.unsorted(); + if (sort != null && !sort.isBlank()) { + // Support multiple sort criteria separated by ';' + // Each criterion can be: "field,asc" or "field:asc" or "-field" + String[] criteria = sort.split(";"); + List orders = new ArrayList<>(); + for (String c : criteria) { + c = c.trim(); + if (c.isEmpty()) continue; + if (c.startsWith("-")) { + orders.add(Sort.Order.desc(c.substring(1))); + } else if (c.contains(",")) { + String[] kv = c.split(",", 2); + String prop = kv[0].trim(); + String dir = kv[1].trim().toLowerCase(); + orders.add("desc".equals(dir) ? Sort.Order.desc(prop) : Sort.Order.asc(prop)); + } else if (c.contains(":")) { + String[] kv = c.split(":", 2); + String prop = kv[0].trim(); + String dir = kv[1].trim().toLowerCase(); + orders.add("desc".equals(dir) ? Sort.Order.desc(prop) : Sort.Order.asc(prop)); + } else { + orders.add(Sort.Order.asc(c)); + } + } + if (!orders.isEmpty()) sortObj = Sort.by(orders); + } + + Pageable pageable = PageRequest.of(page, size, sortObj); + var dtoPage = getRulesUseCase.execute(java.util.Optional.ofNullable(type), java.util.Optional.ofNullable(active), pageable); + var response = new java.util.HashMap(); + response.put("content", dtoPage.getContent()); + response.put("page", dtoPage.getNumber()); + response.put("size", dtoPage.getSize()); + response.put("totalElements", dtoPage.getTotalElements()); + response.put("totalPages", dtoPage.getTotalPages()); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java new file mode 100644 index 0000000..48d63a6 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java @@ -0,0 +1,17 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.UUID; + +@Repository +public interface JpaRuleRepository extends JpaRepository, RuleRepository { + Page findByRuleTypeAndActive(String ruleType, boolean active, Pageable pageable); + Page findByRuleType(String ruleType, Pageable pageable); + Page findByActive(boolean active, Pageable pageable); +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java new file mode 100644 index 0000000..7ecb8a8 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java @@ -0,0 +1,33 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.Optional; + +@Component +public class RuleRepositoryAdapter implements RuleRepository { + + private final JpaRuleRepository jpa; + + public RuleRepositoryAdapter(JpaRuleRepository jpa) { + this.jpa = jpa; + } + + @Override + public Page findAllByTypeAndActive(Optional type, Optional active, Pageable pageable) { + if (type.isPresent() && active.isPresent()) { + return jpa.findByRuleTypeAndActive(type.get(), active.get(), pageable); + } + if (type.isPresent()) { + return jpa.findByRuleType(type.get(), pageable); + } + if (active.isPresent()) { + return jpa.findByActive(active.get(), pageable); + } + return jpa.findAll(pageable); + } +} diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index 2e8006f..eb533a4 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -157,6 +157,44 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/v1/rules: + get: + summary: Get rules + tags: + - Rules + parameters: + - name: page + in: query + schema: + type: integer + default: 0 + - name: size + in: query + schema: + type: integer + default: 20 + - name: sort + in: query + schema: + type: string + - name: type + in: query + schema: + type: string + - name: active + in: query + schema: + type: boolean + responses: + '200': + description: paged list of rules + content: + application/json: + schema: + $ref: '#/components/schemas/PagedRuleDto' + security: + - bearerAuth: [] + components: securitySchemes: bearerAuth: @@ -333,4 +371,47 @@ components: aircraftType: $ref: '#/components/schemas/AircraftTypeInfo' operationalBase: - $ref: '#/components/schemas/OperationalBaseInfo' \ No newline at end of file + $ref: '#/components/schemas/OperationalBaseInfo' + + RuleDto: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + description: + type: string + ruleType: + type: string + maxMonthlyHours: + type: integer + minRestMinutes: + type: integer + minFlights: + type: integer + active: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + + PagedRuleDto: + type: object + properties: + content: + type: array + items: + $ref: '#/components/schemas/RuleDto' + page: + type: integer + size: + type: integer + totalElements: + type: integer + totalPages: + type: integer \ No newline at end of file diff --git a/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql b/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql new file mode 100644 index 0000000..1918fe1 --- /dev/null +++ b/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS rules ( + id UUID PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + rule_type TEXT, + max_monthly_hours INT, + min_rest_minutes INT, + min_flights INT, + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX IF NOT EXISTS idx_rules_rule_type ON rules(rule_type); +CREATE INDEX IF NOT EXISTS idx_rules_active ON rules(active); diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java new file mode 100644 index 0000000..5abc65c --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/application/rules/GetRulesUseCaseTest.java @@ -0,0 +1,25 @@ +package pl.skyroster.skyroster_backend.application.rules; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GetRulesUseCaseTest { + @Test + void returnsMappedPage() { + RuleRepository port = Mockito.mock(RuleRepository.class); + var r = new Rule(UUID.randomUUID(), "TestRule", null, null, null, null, null, true, null, null); + Mockito.when(port.findAllByTypeAndActive(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(new PageImpl<>(List.of(r))); + var uc = new GetRulesUseCase(port); + var page = uc.execute(java.util.Optional.empty(), java.util.Optional.empty(), PageRequest.of(0,10)); + assertEquals(1, page.getTotalElements()); + } +} \ No newline at end of file diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java new file mode 100644 index 0000000..3968c31 --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java @@ -0,0 +1,13 @@ +package pl.skyroster.skyroster_backend.integration; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +// Integration test requires Testcontainers and Docker. Disabled by default in CI-less local run. +@Disabled("Requires Docker/Testcontainers - enable when environment supports it") +public class RulesControllerIT { + @Test + void placeholder() { + // Implement integration test using Testcontainers, Postgres and Keycloak if needed. + } +} From ad008cedb354b5d33cb0a8812db8ea69c762853e Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sat, 9 May 2026 17:27:41 +0200 Subject: [PATCH 02/12] test(SKY-101): enable RulesController integration test (requires Docker/Testcontainers) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../integration/RulesControllerIT.java | 80 +++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java index 3968c31..4f16aa0 100644 --- a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/integration/RulesControllerIT.java @@ -1,13 +1,83 @@ package pl.skyroster.skyroster_backend.integration; -import org.junit.jupiter.api.Disabled; +import dasniko.testcontainers.keycloak.KeycloakContainer; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; +import pl.skyroster.skyroster_backend.TestcontainersConfiguration; +import pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence.JpaRuleRepository; +import pl.skyroster.skyroster_backend.domain.model.Rule; -// Integration test requires Testcontainers and Docker. Disabled by default in CI-less local run. -@Disabled("Requires Docker/Testcontainers - enable when environment supports it") +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.OffsetDateTime; +import java.util.UUID; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Import(TestcontainersConfiguration.class) public class RulesControllerIT { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakContainer keycloak; + + @Autowired + private JpaRuleRepository ruleRepository; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + @Test - void placeholder() { - // Implement integration test using Testcontainers, Postgres and Keycloak if needed. + void getRules_returnsSavedRule_forComplianceRole() throws Exception { + // prepare data + Rule r = new Rule(UUID.randomUUID(), "IT-Test-Rule", "desc", "MAX_MONTHLY_HOURS", 160, 720, 5, true, OffsetDateTime.now(), null); + ruleRepository.save(r); + + String token = getToken("compliance", "test1234"); + + mockMvc.perform(get("/api/v1/rules") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].name").value("IT-Test-Rule")); + } + + private String getToken(String username, String password) throws Exception { + String tokenUrl = keycloak.getAuthServerUrl() + "/realms/skyroster/protocol/openid-connect/token"; + + String body = "grant_type=password" + + "&client_id=skyroster-frontend" + + "&username=" + username + + "&password=" + password; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(tokenUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Failed to get token: " + response.body()); + } + + JsonNode json = objectMapper.readTree(response.body()); + return json.get("access_token").asText(); } } From a7663a38696443af08885ae0cefc5b4384e15121 Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sat, 9 May 2026 17:30:11 +0200 Subject: [PATCH 03/12] fix(SKY-101): avoid duplicate RuleRepository beans by not extending domain port in JpaRuleRepository Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../adapter/out/persistence/JpaRuleRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java index 48d63a6..55e4b76 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java @@ -10,7 +10,7 @@ import java.util.UUID; @Repository -public interface JpaRuleRepository extends JpaRepository, RuleRepository { +public interface JpaRuleRepository extends JpaRepository { Page findByRuleTypeAndActive(String ruleType, boolean active, Pageable pageable); Page findByRuleType(String ruleType, Pageable pageable); Page findByActive(boolean active, Pageable pageable); From 2606470dd447477315537e05eeeb0cbe008304ad Mon Sep 17 00:00:00 2001 From: krzysztofgorski Date: Sun, 10 May 2026 12:16:15 +0200 Subject: [PATCH 04/12] endpoint na get --- infrastructure/local/docker-compose.yaml | 31 +++++++- .../local/postgres/init-databases.sh | 0 .../persistence/RuleRepositoryAdapter.java | 76 +++++++++++++++---- .../config/security/SecurityConfig.java | 1 + .../src/main/resources/api/openapi.yaml | 2 +- ...s_table.sql => V3__create_rules_table.sql} | 2 + 6 files changed, 94 insertions(+), 18 deletions(-) mode change 100644 => 100755 infrastructure/local/postgres/init-databases.sh rename skyroster-backend/src/main/resources/db/migration/{V20260509__create_rules_table.sql => V3__create_rules_table.sql} (99%) diff --git a/infrastructure/local/docker-compose.yaml b/infrastructure/local/docker-compose.yaml index ec3481b..024b543 100644 --- a/infrastructure/local/docker-compose.yaml +++ b/infrastructure/local/docker-compose.yaml @@ -24,6 +24,28 @@ services: timeout: 3s retries: 5 + keycloak-db: + image: postgres:17 + container_name: skyroster-keycloak-db + networks: + - skyroster-network + profiles: + - backend + - full + environment: + POSTGRES_DB: ${KEYCLOAK_DB_NAME:-keycloak} + POSTGRES_USER: ${KEYCLOAK_DB_USER:-keycloak} + POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} + ports: + - "5433:5432" + volumes: + - keycloak_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-keycloak}"] + interval: 5s + timeout: 3s + retries: 5 + keycloak: image: quay.io/keycloak/keycloak:26.2 container_name: skyroster-keycloak @@ -35,9 +57,9 @@ services: - full environment: KC_DB: postgres - KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak - KC_DB_USERNAME: ${DB_USERNAME:-skyroster} - KC_DB_PASSWORD: ${DB_PASSWORD:-skyroster} + KC_DB_URL: jdbc:postgresql://keycloak-db:5432/${KEYCLOAK_DB_NAME:-keycloak} + KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak} + KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} ports: @@ -46,7 +68,7 @@ services: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json command: start-dev --import-realm depends_on: - postgres: + keycloak-db: condition: service_healthy frontend: @@ -95,6 +117,7 @@ services: volumes: pgdata: + keycloak_pgdata: networks: skyroster-network: diff --git a/infrastructure/local/postgres/init-databases.sh b/infrastructure/local/postgres/init-databases.sh old mode 100644 new mode 100755 diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java index 7ecb8a8..4171297 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/RuleRepositoryAdapter.java @@ -1,33 +1,83 @@ package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; import pl.skyroster.skyroster_backend.domain.model.Rule; import pl.skyroster.skyroster_backend.domain.port.RuleRepository; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +/** + * Adapter returning a fixed set of predefined rules (in-memory). + * This makes rules "hard-coded" instead of being read from DB. + */ @Component public class RuleRepositoryAdapter implements RuleRepository { - private final JpaRuleRepository jpa; + private static final List PREDEFINED; - public RuleRepositoryAdapter(JpaRuleRepository jpa) { - this.jpa = jpa; + static { + List list = new ArrayList<>(); + + // Example additional fixed rules + list.add(new Rule( + UUID.fromString("11111111-1111-1111-1111-111111111111"), + "Min Rest 12h", + "Minimum rest time between duties", + "MIN_REST_MINUTES", + null, + 720, + null, + true, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + null + )); + list.add(new Rule( + UUID.fromString("22222222-2222-2222-2222-222222222222"), + "Min Flights", + "Minimum number of flights per month", + "MIN_FLIGHTS", + null, + null, + 3, + true, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + null + )); + list.add(new Rule( + UUID.fromString("33333333-3333-3333-3333-333333333333"), + "Max Month Hours", + "Maximum flight hours per month", + "MAX_MONTHLY_HOURS", + 160, // przykładowy limit godzin + null, + null, + true, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + null + )); + + PREDEFINED = List.copyOf(list); } @Override public Page findAllByTypeAndActive(Optional type, Optional active, Pageable pageable) { - if (type.isPresent() && active.isPresent()) { - return jpa.findByRuleTypeAndActive(type.get(), active.get(), pageable); - } - if (type.isPresent()) { - return jpa.findByRuleType(type.get(), pageable); - } - if (active.isPresent()) { - return jpa.findByActive(active.get(), pageable); - } - return jpa.findAll(pageable); + List filtered = PREDEFINED.stream() + .filter(r -> type.map(t -> t.equalsIgnoreCase(r.getRuleType())).orElse(true)) + .filter(r -> active.map(a -> a.equals(r.isActive())).orElse(true)) + .collect(Collectors.toList()); + + int total = filtered.size(); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), total); + List pageContent = start >= total ? List.of() : filtered.subList(start, end); + return new PageImpl<>(pageContent, pageable, total); } } diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java index 2df005b..aba9915 100644 --- a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/config/security/SecurityConfig.java @@ -46,6 +46,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) { .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/api/compliance/**").hasRole("compliance_officer") .requestMatchers("/api/planning/**").hasRole("schedule_planner") + .requestMatchers("/api/v1/rules/**").hasRole("compliance_officer") .requestMatchers(HttpMethod.POST, "/api/aircraft/**").hasRole("operations_administrator") .requestMatchers(HttpMethod.GET, "/api/aircraft/**").authenticated() .requestMatchers("/api/**").authenticated() diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index eb533a4..0ffbd13 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -157,7 +157,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /api/v1/rules: + /api/rules: get: summary: Get rules tags: diff --git a/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql b/skyroster-backend/src/main/resources/db/migration/V3__create_rules_table.sql similarity index 99% rename from skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql rename to skyroster-backend/src/main/resources/db/migration/V3__create_rules_table.sql index 1918fe1..8b35690 100644 --- a/skyroster-backend/src/main/resources/db/migration/V20260509__create_rules_table.sql +++ b/skyroster-backend/src/main/resources/db/migration/V3__create_rules_table.sql @@ -11,5 +11,7 @@ CREATE TABLE IF NOT EXISTS rules ( updated_at TIMESTAMP WITH TIME ZONE ); + + CREATE INDEX IF NOT EXISTS idx_rules_rule_type ON rules(rule_type); CREATE INDEX IF NOT EXISTS idx_rules_active ON rules(active); From 864c874028c10b611e0e0d1ed5a8ba872c7289ce Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:17:34 +0200 Subject: [PATCH 05/12] feat(SKY-25): add OpenAPI spec for GET /api/rules --- .../src/main/resources/api/openapi.yaml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/skyroster-backend/src/main/resources/api/openapi.yaml b/skyroster-backend/src/main/resources/api/openapi.yaml index cb1c775..474b1c3 100644 --- a/skyroster-backend/src/main/resources/api/openapi.yaml +++ b/skyroster-backend/src/main/resources/api/openapi.yaml @@ -248,6 +248,27 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/rules: + get: + operationId: getRules + tags: [ Rules ] + summary: List operational rules + responses: + '200': + description: List of operational rules + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RuleResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/flights: get: operationId: displayFlights @@ -793,6 +814,45 @@ components: operationalBase: $ref: '#/components/schemas/OperationalBaseInfo' + RuleType: + type: string + enum: + - MAX_WORK_TIME + - MIN_REST_TIME + - MIN_FLIGHT_TIME + + RulePeriod: + type: string + enum: + - DAY + - WEEK + - MONTH + + RuleResponse: + type: object + required: + - id + - name + - type + - value + - period + properties: + id: + type: string + format: uuid + name: + type: string + type: + $ref: '#/components/schemas/RuleType' + value: + type: integer + minimum: 1 + description: Value in hours + period: + $ref: '#/components/schemas/RulePeriod' + description: + type: string + FlightResponse: type: object required: From fb5f98e4b2abf8e29625f07432bc1823a963c073 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:05 +0200 Subject: [PATCH 06/12] feat(SKY-25): add Rule domain model and repository port --- .../skyroster_backend/domain/model/Rule.java | 41 +++++++++++++++++++ .../domain/model/RulePeriod.java | 7 ++++ .../domain/model/RuleType.java | 7 ++++ .../domain/port/RuleRepository.java | 9 ++++ 4 files changed, 64 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java new file mode 100644 index 0000000..36ee6ec --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/Rule.java @@ -0,0 +1,41 @@ +package pl.skyroster.skyroster_backend.domain.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Table(name = "rules") +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Rule { + + @Id + private UUID id; + + @Column(nullable = false, length = 100) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 30) + private RuleType type; + + @Column(nullable = false) + private Integer value; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 10) + private RulePeriod period; + + @Column(length = 500) + private String description; +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java new file mode 100644 index 0000000..0ea8f93 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RulePeriod.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.model; + +public enum RulePeriod { + DAY, + WEEK, + MONTH +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java new file mode 100644 index 0000000..7fa13e3 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/model/RuleType.java @@ -0,0 +1,7 @@ +package pl.skyroster.skyroster_backend.domain.model; + +public enum RuleType { + MAX_WORK_TIME, + MIN_REST_TIME, + MIN_FLIGHT_TIME +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java new file mode 100644 index 0000000..b5a45d0 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/domain/port/RuleRepository.java @@ -0,0 +1,9 @@ +package pl.skyroster.skyroster_backend.domain.port; + +import pl.skyroster.skyroster_backend.domain.model.Rule; + +import java.util.List; + +public interface RuleRepository { + List findAll(); +} From b41bf4112a5ef7a549de2fed2ed18342621c60a9 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:18 +0200 Subject: [PATCH 07/12] feat(SKY-25): add rules table migration with seed data --- .../migration/V3__rules_schema_and_seed.sql | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql diff --git a/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql b/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql new file mode 100644 index 0000000..e49fa0f --- /dev/null +++ b/skyroster-backend/src/main/resources/db/migration/V3__rules_schema_and_seed.sql @@ -0,0 +1,22 @@ +CREATE TABLE rules ( + id UUID PRIMARY KEY, + name VARCHAR(100) NOT NULL, + type VARCHAR(30) NOT NULL, + value INTEGER NOT NULL CHECK (value > 0), + period VARCHAR(10) NOT NULL, + description VARCHAR(500) +); + +INSERT INTO rules (id, name, type, value, period, description) VALUES + ('11111111-0000-0000-0000-000000000001', 'Maksymalny czas pracy miesięczny', + 'MAX_WORK_TIME', 100, 'MONTH', + 'Pilot nie może przekroczyć 100 godzin pracy w miesiącu'), + ('11111111-0000-0000-0000-000000000002', 'Maksymalny czas pracy dzienny', + 'MAX_WORK_TIME', 10, 'DAY', + 'Pilot nie może przekroczyć 10 godzin pracy dziennie'), + ('11111111-0000-0000-0000-000000000003', 'Minimalny odpoczynek między lotami', + 'MIN_REST_TIME', 12, 'DAY', + 'Wymagany minimalny odpoczynek 12 godzin między lotami'), + ('11111111-0000-0000-0000-000000000004', 'Minimalny nalot miesięczny', + 'MIN_FLIGHT_TIME', 10, 'MONTH', + 'Pilot musi wykonać minimum 10 godzin nalotu miesięcznie'); From 310ec1eb76bc4621f1bd2e1c334fbb50da6147a4 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:34 +0200 Subject: [PATCH 08/12] feat(SKY-25): add JPA adapter for RuleRepository --- .../adapter/out/persistence/JpaRuleRepository.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java new file mode 100644 index 0000000..23fdc7d --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/out/persistence/JpaRuleRepository.java @@ -0,0 +1,10 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.UUID; + +public interface JpaRuleRepository extends JpaRepository, RuleRepository { +} From 72194a115e5e5c5ed7781179acc1377945b2e6f2 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:18:47 +0200 Subject: [PATCH 09/12] feat(SKY-25): add GetRulesUseCase --- .../application/rule/GetRulesUseCase.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java new file mode 100644 index 0000000..2909a23 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/application/rule/GetRulesUseCase.java @@ -0,0 +1,23 @@ +package pl.skyroster.skyroster_backend.application.rule; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.domain.port.RuleRepository; + +import java.util.List; + +@Service +public class GetRulesUseCase { + + private final RuleRepository ruleRepository; + + public GetRulesUseCase(RuleRepository ruleRepository) { + this.ruleRepository = ruleRepository; + } + + @Transactional(readOnly = true) + public List execute() { + return ruleRepository.findAll(); + } +} From 69eb3243f0738186d57f831a55456b24a00311da Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:19:33 +0200 Subject: [PATCH 10/12] feat(SKY-25): add RuleController with GET /api/rules --- .../adapter/in/web/RuleController.java | 27 +++++++++++++++++++ .../mappers/RuleResponseMapper.java | 18 +++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java create mode 100644 skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java new file mode 100644 index 0000000..a4c8217 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleController.java @@ -0,0 +1,27 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import pl.skyroster.skyroster_backend.application.rule.GetRulesUseCase; +import pl.skyroster.skyroster_backend.generated.api.ApiApi; +import pl.skyroster.skyroster_backend.generated.model.RuleResponse; +import pl.skyroster.skyroster_backend.infrastructure.mappers.RuleResponseMapper; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +public class RuleController { + + private final GetRulesUseCase getRulesUseCase; + + @GetMapping(ApiApi.PATH_GET_RULES) + public ResponseEntity> getRules() { + List response = getRulesUseCase.execute().stream() + .map(RuleResponseMapper::map) + .toList(); + return ResponseEntity.ok(response); + } +} diff --git a/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java new file mode 100644 index 0000000..6377711 --- /dev/null +++ b/skyroster-backend/src/main/java/pl/skyroster/skyroster_backend/infrastructure/mappers/RuleResponseMapper.java @@ -0,0 +1,18 @@ +package pl.skyroster.skyroster_backend.infrastructure.mappers; + +import pl.skyroster.skyroster_backend.domain.model.Rule; +import pl.skyroster.skyroster_backend.generated.model.RulePeriod; +import pl.skyroster.skyroster_backend.generated.model.RuleResponse; +import pl.skyroster.skyroster_backend.generated.model.RuleType; + +public class RuleResponseMapper { + public static RuleResponse map(Rule rule) { + return new RuleResponse( + rule.getId(), + rule.getName(), + RuleType.valueOf(rule.getType().name()), + rule.getValue(), + RulePeriod.valueOf(rule.getPeriod().name()) + ).description(rule.getDescription()); + } +} From 822e64f561ce878709a7ce8ad0b4ed7ee68c0746 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:24:28 +0200 Subject: [PATCH 11/12] test(SKY-25): add integration tests for GET /api/rules --- .../adapter/in/web/RuleIntegrationTest.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java diff --git a/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java new file mode 100644 index 0000000..2734b6c --- /dev/null +++ b/skyroster-backend/src/test/java/pl/skyroster/skyroster_backend/infrastructure/adapter/in/web/RuleIntegrationTest.java @@ -0,0 +1,91 @@ +package pl.skyroster.skyroster_backend.infrastructure.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dasniko.testcontainers.keycloak.KeycloakContainer; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; +import pl.skyroster.skyroster_backend.TestcontainersConfiguration; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Import(TestcontainersConfiguration.class) +class RuleIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private KeycloakContainer keycloak; + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void getRules_shouldReturn401_whenNoToken() throws Exception { + mockMvc.perform(get("/api/rules")) + .andExpect(status().isUnauthorized()); + } + + @Test + void getRules_shouldReturnSeededRules_whenAuthenticated() throws Exception { + String token = getToken("admin", "test1234"); + + mockMvc.perform(get("/api/rules") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$", hasSize(4))) + .andExpect(jsonPath( + "$[?(@.name == 'Maksymalny czas pracy miesięczny' && @.type == 'MAX_WORK_TIME' && @.value == 100 && @.period == 'MONTH')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Maksymalny czas pracy dzienny' && @.type == 'MAX_WORK_TIME' && @.value == 10 && @.period == 'DAY')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Minimalny odpoczynek między lotami' && @.type == 'MIN_REST_TIME' && @.value == 12 && @.period == 'DAY')]") + .exists()) + .andExpect(jsonPath( + "$[?(@.name == 'Minimalny nalot miesięczny' && @.type == 'MIN_FLIGHT_TIME' && @.value == 10 && @.period == 'MONTH')]") + .exists()); + } + + private String getToken(String username, String password) throws Exception { + String tokenUrl = keycloak.getAuthServerUrl() + "/realms/skyroster/protocol/openid-connect/token"; + + String reqBody = "grant_type=password" + + "&client_id=skyroster-frontend" + + "&username=" + username + + "&password=" + password; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(tokenUrl)) + .header("Content-Type", "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(reqBody)) + .build(); + + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Failed to get token: " + response.body()); + } + + JsonNode json = objectMapper.readTree(response.body()); + return json.get("access_token").asText(); + } +} From b8f7334c20b7c73b9224311d31e573d843e6eb14 Mon Sep 17 00:00:00 2001 From: Krywion Date: Sat, 16 May 2026 00:41:31 +0200 Subject: [PATCH 12/12] revert(SKY-25): drop docker-compose Keycloak DB split from SKY-101 merge Not needed for this task; reverts infrastructure/local/docker-compose.yaml and init-databases.sh mode change to match main. --- infrastructure/local/docker-compose.yaml | 31 +++---------------- .../local/postgres/init-databases.sh | 0 2 files changed, 4 insertions(+), 27 deletions(-) mode change 100755 => 100644 infrastructure/local/postgres/init-databases.sh diff --git a/infrastructure/local/docker-compose.yaml b/infrastructure/local/docker-compose.yaml index 024b543..ec3481b 100644 --- a/infrastructure/local/docker-compose.yaml +++ b/infrastructure/local/docker-compose.yaml @@ -24,28 +24,6 @@ services: timeout: 3s retries: 5 - keycloak-db: - image: postgres:17 - container_name: skyroster-keycloak-db - networks: - - skyroster-network - profiles: - - backend - - full - environment: - POSTGRES_DB: ${KEYCLOAK_DB_NAME:-keycloak} - POSTGRES_USER: ${KEYCLOAK_DB_USER:-keycloak} - POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} - ports: - - "5433:5432" - volumes: - - keycloak_pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-keycloak}"] - interval: 5s - timeout: 3s - retries: 5 - keycloak: image: quay.io/keycloak/keycloak:26.2 container_name: skyroster-keycloak @@ -57,9 +35,9 @@ services: - full environment: KC_DB: postgres - KC_DB_URL: jdbc:postgresql://keycloak-db:5432/${KEYCLOAK_DB_NAME:-keycloak} - KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak} - KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} + KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak + KC_DB_USERNAME: ${DB_USERNAME:-skyroster} + KC_DB_PASSWORD: ${DB_PASSWORD:-skyroster} KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} ports: @@ -68,7 +46,7 @@ services: - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json command: start-dev --import-realm depends_on: - keycloak-db: + postgres: condition: service_healthy frontend: @@ -117,7 +95,6 @@ services: volumes: pgdata: - keycloak_pgdata: networks: skyroster-network: diff --git a/infrastructure/local/postgres/init-databases.sh b/infrastructure/local/postgres/init-databases.sh old mode 100755 new mode 100644